diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1cb6e28..81c5761 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,7 @@ +#### 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 + #### 8.7.0 - April 19, 2026 - Tests: Add `GenerativeMethodsTests` covering instance/static methods and method-count in generative types #505 diff --git a/tests/FSharp.TypeProviders.SDK.Tests.fsproj b/tests/FSharp.TypeProviders.SDK.Tests.fsproj index 6856cde..9f39319 100644 --- a/tests/FSharp.TypeProviders.SDK.Tests.fsproj +++ b/tests/FSharp.TypeProviders.SDK.Tests.fsproj @@ -25,6 +25,7 @@ + diff --git a/tests/GenerativeInheritanceTests.fs b/tests/GenerativeInheritanceTests.fs new file mode 100644 index 0000000..ae6b402 --- /dev/null +++ b/tests/GenerativeInheritanceTests.fs @@ -0,0 +1,161 @@ +module TPSDK.GenerativeInheritanceTests + +// Tests for generative type inheritance: one ProvidedTypeDefinition deriving from +// another, with virtual/abstract method dispatch verified at runtime. +// +// Scenario: +// abstract class Animal { abstract Speak() : string } +// class Dog(_name:string) extends Animal { override Speak() = "Woof! I am " + _name } +// class Cat(_name:string) extends Animal { override Speak() = "Meow! I am " + _name } + +#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 GenerativeInheritanceProvider (config: TypeProviderConfig) as this = + inherit TypeProviderForNamespaces (config) + + let ns = "Inheritance.Provided" + let tempAssembly = ProvidedAssembly() + let container = ProvidedTypeDefinition(tempAssembly, ns, "Animals", Some typeof, isErased = false) + + do + // Abstract base class: Animal + let animalType = + ProvidedTypeDefinition("Animal", Some typeof, + isErased = false, isAbstract = true, isSealed = false) + + // Abstract method - no invokeCode; the TPSDK auto-applies Abstract|Virtual when class is abstract + let speakMethod = ProvidedMethod("Speak", [], typeof) + animalType.AddMember speakMethod + // Default constructor so derived classes can chain to it + animalType.AddMember (ProvidedConstructor([], invokeCode = fun _ -> <@@ () @@>)) + + // Concrete class: Dog + let dogType = + ProvidedTypeDefinition("Dog", Some (animalType :> Type), + isErased = false, isSealed = false) + + let dogNameField = ProvidedField("_name", typeof) + dogType.AddMember dogNameField + + dogType.AddMember + (ProvidedConstructor( + [ProvidedParameter("name", typeof)], + invokeCode = fun args -> + Expr.FieldSetUnchecked(args.[0], dogNameField, args.[1]))) + + let dogSpeak = + ProvidedMethod("Speak", [], typeof, isStatic = false, + invokeCode = fun args -> + let nameExpr = Expr.FieldGetUnchecked(args.[0], dogNameField) + <@@ "Woof! I am " + (%%nameExpr : string) @@>) + dogSpeak.AddMethodAttrs(MethodAttributes.Virtual ||| MethodAttributes.HideBySig) + dogType.AddMember dogSpeak + dogType.DefineMethodOverride(dogSpeak, speakMethod) + + // Concrete class: Cat + let catType = + ProvidedTypeDefinition("Cat", Some (animalType :> Type), + isErased = false, isSealed = false) + + let catNameField = ProvidedField("_name", typeof) + catType.AddMember catNameField + + catType.AddMember + (ProvidedConstructor( + [ProvidedParameter("name", typeof)], + invokeCode = fun args -> + Expr.FieldSetUnchecked(args.[0], catNameField, args.[1]))) + + let catSpeak = + ProvidedMethod("Speak", [], typeof, isStatic = false, + invokeCode = fun args -> + let nameExpr = Expr.FieldGetUnchecked(args.[0], catNameField) + <@@ "Meow! I am " + (%%nameExpr : string) @@>) + catSpeak.AddMethodAttrs(MethodAttributes.Virtual ||| MethodAttributes.HideBySig) + catType.AddMember catSpeak + catType.DefineMethodOverride(catSpeak, speakMethod) + + container.AddMembers [animalType; dogType; catType] + 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 = GenerativeInheritanceProvider(cfg) :> TypeProviderForNamespaces + let providedType = tp.Namespaces.[0].GetTypes().[0] + let bytes = (tp :> ITypeProvider).GetGeneratedAssemblyContents(providedType.Assembly) + let assem = Assembly.Load bytes + assem.GetType("Inheritance.Provided.Animals") + +[] +let ``Generative Dog type is a subclass of generative Animal type``() = + let animals = loadTestAssembly() + let animalType = animals.GetNestedType("Animal") + let dogType = animals.GetNestedType("Dog") + Assert.NotNull(animalType) + Assert.NotNull(dogType) + Assert.True(dogType.IsSubclassOf(animalType), "Dog should be a subclass of Animal") + +[] +let ``Generative Animal Speak method is abstract``() = + let animals = loadTestAssembly() + let animalType = animals.GetNestedType("Animal") + let speakMethod = animalType.GetMethod("Speak", BindingFlags.Instance ||| BindingFlags.Public) + Assert.NotNull(speakMethod) + Assert.True(speakMethod.IsAbstract, "Animal.Speak should be abstract") + Assert.True(speakMethod.IsVirtual, "Animal.Speak should be virtual") + +[] +let ``Generative Dog Speak method returns expected string``() = + let animals = loadTestAssembly() + let dogType = animals.GetNestedType("Dog") + Assert.NotNull(dogType) + let ctor = dogType.GetConstructor([| typeof |]) + Assert.NotNull(ctor) + let dog = ctor.Invoke([| box "Buddy" |]) + let speakMethod = dogType.GetMethod("Speak", BindingFlags.Instance ||| BindingFlags.Public) + Assert.NotNull(speakMethod) + let result = speakMethod.Invoke(dog, [||]) :?> string + Assert.Equal("Woof! I am Buddy", result) + +[] +let ``Generative Cat Speak method returns expected string``() = + let animals = loadTestAssembly() + let catType = animals.GetNestedType("Cat") + Assert.NotNull(catType) + let ctor = catType.GetConstructor([| typeof |]) + Assert.NotNull(ctor) + let cat = ctor.Invoke([| box "Whiskers" |]) + let speakMethod = catType.GetMethod("Speak", BindingFlags.Instance ||| BindingFlags.Public) + let result = speakMethod.Invoke(cat, [||]) :?> string + Assert.Equal("Meow! I am Whiskers", result) + +[] +let ``Generative Speak override is dispatched polymorphically via Animal base reference``() = + let animals = loadTestAssembly() + let animalType = animals.GetNestedType("Animal") + let dogType = animals.GetNestedType("Dog") + let catType = animals.GetNestedType("Cat") + let speakViaBase = animalType.GetMethod("Speak", BindingFlags.Instance ||| BindingFlags.Public) + Assert.NotNull(speakViaBase) + + let dog = dogType.GetConstructor([| typeof |]).Invoke([| box "Rex" |]) + let cat = catType.GetConstructor([| typeof |]).Invoke([| box "Luna" |]) + + // Invoke via the base-class MethodInfo — exercises virtual dispatch + let dogResult = speakViaBase.Invoke(dog, [||]) :?> string + let catResult = speakViaBase.Invoke(cat, [||]) :?> string + Assert.Equal("Woof! I am Rex", dogResult) + Assert.Equal("Meow! I am Luna", catResult)