Skip to content

Commit c843603

Browse files
[Repo Assist] fix: ProvidedTypeDefinition.Logger broken (new ref each call); add 5 tests for warning + enum round-trip (#501)
🤖 *This PR was created by [Repo Assist](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md), an automated AI assistant.* ## Summary This PR bundles a **bug fix** and **new tests** (Task 9: Testing Improvements). ### Bug fix: `ProvidedTypeDefinition.Logger` was silently broken The static `Logger` member was implemented as a computed property: ```fsharp // Before — creates a fresh ref cell on every access; impossible to set static member Logger: (string -> unit) option ref = ref None ``` Every read of `ProvidedTypeDefinition.Logger` returned a *different* `ref` object, so calling `ProvidedTypeDefinition.Logger := Some f` would write into a temporary cell that is immediately discarded. The Logger was effectively inoperative: the "all static parameters optional" warning (added in 8.3.0 / PR #428) could never fire via the Logger, and type-creation trace messages were silently dropped. **Fix**: introduces a `static let loggerRef` field and makes `Logger` return it: ```fsharp static let loggerRef: (string -> unit) option ref = ref None ... static member Logger: (string -> unit) option ref = loggerRef ``` ### New tests (5 tests, 131 total) **`BasicErasedProvisionTests.fs` — 3 new tests for the all-optional warning:** - `DefineStaticParameters warns when all static parameters have defaults` — regression for the warning path (which was previously unreachable due to the Logger bug) - `DefineStaticParameters does not warn when at least one parameter has no default` - `DefineStaticParameters does not warn when there are no static parameters` **`GenerativeEnumsProvisionTests.fs` — 2 new tests for non-Int32 enum round-trip via target context:** - `Byte enum underlying type is correct when read via target context (ReadRelatedAssembly)` — exercises `TargetTypeDefinition.GetEnumUnderlyingType()` for a `byte`-backed enum - `Int64 enum underlying type is correct when read via target context (ReadRelatedAssembly)` — same for `int64`-backed enums These complement the existing runtime-path enum tests (`Assembly.Load`) by covering the design-time IL-reader path, which was fixed in PR #470/#475. ## Trade-offs - The Logger ref is now module-level shared state. Tests that manipulate it should save/restore the previous value (all three new tests do this via `try/finally`). ## Test Status ✅ All 131 tests pass (`dotnet test tests/FSharp.TypeProviders.SDK.Tests.fsproj -c Release`) > Generated by 🌈 Repo Assist, see [workflow run](https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/24270385678). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md). > > To install this [agentic workflow](https://github.com/githubnext/agentics/blob/7ee2b60744abf71b985bead4599640f165edcd93/workflows/repo-assist.md), run > ``` > gh aw add githubnext/agentics@7ee2b60 > ``` <!-- gh-aw-agentic-workflow: Repo Assist, engine: copilot, model: auto, id: 24270385678, workflow_id: repo-assist, run: https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/24270385678 --> <!-- gh-aw-workflow-id: repo-assist --> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent b5323d0 commit c843603

3 files changed

Lines changed: 105 additions & 1 deletion

File tree

src/ProvidedTypes.fs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,9 @@ and ProvidedTypeDefinition(isTgt: bool, container:TypeContainer, className: stri
13901390
| TypeContainer.Namespace _, Some logger when not isTgt -> logger (sprintf "Creating ProvidedTypeDefinition %s [%d]" className (System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode this))
13911391
| _ -> ()
13921392

1393+
// Shared mutable logger cell; must be a static let so the same ref is returned on every access.
1394+
static let loggerRef: (string -> unit) option ref = ref None
1395+
13931396
static let defaultAttributes (isErased, isSealed, isInterface, isAbstract, isStruct) =
13941397
TypeAttributes.Public |||
13951398
(if isInterface then TypeAttributes.Interface ||| TypeAttributes.Abstract
@@ -1940,7 +1943,7 @@ and ProvidedTypeDefinition(isTgt: bool, container:TypeContainer, className: stri
19401943
| :? ProvidedField as l -> l.PatchDeclaringType this
19411944
| _ -> ()
19421945

1943-
static member Logger: (string -> unit) option ref = ref None
1946+
static member Logger: (string -> unit) option ref = loggerRef
19441947

19451948

19461949
//====================================================================================================

tests/BasicErasedProvisionTests.fs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,3 +811,60 @@ let ``GetUnionCases works on option of provided type`` () =
811811
Assert.Equal(2, cases.Length)
812812
Assert.Equal("None", cases.[0].Name)
813813
Assert.Equal("Some", cases.[1].Name)
814+
815+
// ---------------------------------------------------------------------------
816+
// Tests for DefineStaticParameters warning: all optional parameters
817+
// Addresses PR #428 — warn when every static param has a default value.
818+
// ---------------------------------------------------------------------------
819+
820+
[<Fact>]
821+
let ``DefineStaticParameters warns when all static parameters have defaults``() =
822+
let warnings = System.Collections.Generic.List<string>()
823+
let prevLogger = !ProvidedTypeDefinition.Logger
824+
ProvidedTypeDefinition.Logger := Some warnings.Add
825+
try
826+
let asm = Assembly.GetExecutingAssembly()
827+
let t = ProvidedTypeDefinition(asm, "Test.Warning", "AllOptionalType", Some typeof<obj>)
828+
warnings.Clear() // discard the "Creating..." trace message from the ctor
829+
let p1 = ProvidedStaticParameter("X", typeof<int>, 0)
830+
let p2 = ProvidedStaticParameter("Y", typeof<string>, "default")
831+
t.DefineStaticParameters([p1; p2], fun typeName _args ->
832+
ProvidedTypeDefinition(asm, "Test.Warning", typeName, Some typeof<obj>))
833+
let w = warnings |> Seq.tryFind (fun msg -> msg.Contains("AllOptionalType") && msg.Contains("optional"))
834+
Assert.True(w.IsSome, sprintf "Expected all-optional-params warning, got: %A" (warnings |> Seq.toList))
835+
finally
836+
ProvidedTypeDefinition.Logger := prevLogger
837+
838+
[<Fact>]
839+
let ``DefineStaticParameters does not warn when at least one parameter has no default``() =
840+
let warnings = System.Collections.Generic.List<string>()
841+
let prevLogger = !ProvidedTypeDefinition.Logger
842+
ProvidedTypeDefinition.Logger := Some warnings.Add
843+
try
844+
let asm = Assembly.GetExecutingAssembly()
845+
let t = ProvidedTypeDefinition(asm, "Test.Warning", "MixedParamsType", Some typeof<obj>)
846+
warnings.Clear()
847+
let required = ProvidedStaticParameter("Required", typeof<int>)
848+
let optional = ProvidedStaticParameter("Optional", typeof<string>, "default")
849+
t.DefineStaticParameters([required; optional], fun typeName _args ->
850+
ProvidedTypeDefinition(asm, "Test.Warning", typeName, Some typeof<obj>))
851+
let w = warnings |> Seq.tryFind (fun msg -> msg.Contains("MixedParamsType") && msg.Contains("optional"))
852+
Assert.True(w.IsNone, sprintf "No all-optional warning expected for mixed params, got: %A" (warnings |> Seq.toList))
853+
finally
854+
ProvidedTypeDefinition.Logger := prevLogger
855+
856+
[<Fact>]
857+
let ``DefineStaticParameters does not warn when there are no static parameters``() =
858+
let warnings = System.Collections.Generic.List<string>()
859+
let prevLogger = !ProvidedTypeDefinition.Logger
860+
ProvidedTypeDefinition.Logger := Some warnings.Add
861+
try
862+
let asm = Assembly.GetExecutingAssembly()
863+
let t = ProvidedTypeDefinition(asm, "Test.Warning", "NoParamsType", Some typeof<obj>)
864+
warnings.Clear()
865+
t.DefineStaticParameters([], fun typeName _args ->
866+
ProvidedTypeDefinition(asm, "Test.Warning", typeName, Some typeof<obj>))
867+
let w = warnings |> Seq.tryFind (fun msg -> msg.Contains("NoParamsType") && msg.Contains("optional"))
868+
Assert.True(w.IsNone, "No all-optional warning expected for empty parameter list")
869+
finally
870+
ProvidedTypeDefinition.Logger := prevLogger

tests/GenerativeEnumsProvisionTests.fs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ let makeAssembly (tp: TypeProviderForNamespaces) =
145145
let assemContents = (tp :> ITypeProvider).GetGeneratedAssemblyContents(providedType.Assembly)
146146
Assembly.Load assemContents
147147

148+
let makeTargetAssembly (tp: TypeProviderForNamespaces) =
149+
let ns = tp.Namespaces.[0]
150+
let providedType = ns.GetTypes().[0]
151+
let assemContents = (tp :> ITypeProvider).GetGeneratedAssemblyContents(providedType.Assembly)
152+
tp.TargetContext.ReadRelatedAssembly(assemContents)
153+
148154
[<Fact>]
149155
let ``Generative enum with byte underlying type is generated correctly``() =
150156
let runtimeAssemblyRefs = Targets.DotNetStandard20FSharpRefs()
@@ -185,3 +191,41 @@ let ``Generative enum with int64 underlying type is generated correctly``() =
185191
let values = Enum.GetValues(int64Enum) |> Seq.cast<int64> |> Seq.zip (Enum.GetNames(int64Enum)) |> Seq.toList
186192
Assert.Equal<(string * int64) list>([("Zero", 0L); ("One", 1L); ("BigVal", 3000000000L)], values)
187193

194+
// ---------------------------------------------------------------------------
195+
// Tests that read enums back via TargetContext.ReadRelatedAssembly (the IL binary
196+
// reader path through TargetTypeDefinition), exercising GetEnumUnderlyingType()
197+
// on non-Int32 enum types via the target context.
198+
// ---------------------------------------------------------------------------
199+
200+
[<Fact>]
201+
let ``Byte enum underlying type is correct when read via target context (ReadRelatedAssembly)``() =
202+
let runtimeAssemblyRefs = Targets.DotNetStandard20FSharpRefs()
203+
let runtimeAssembly = runtimeAssemblyRefs.[0]
204+
let cfg = Testing.MakeSimulatedTypeProviderConfig (__SOURCE_DIRECTORY__, runtimeAssembly, runtimeAssemblyRefs)
205+
let tp = GenerativeByteEnumsProvider(cfg) :> TypeProviderForNamespaces
206+
let targetAssem = makeTargetAssembly tp
207+
208+
let container = targetAssem.GetType("ByteEnums.Provided.Container")
209+
Assert.NotNull(container)
210+
211+
let byteEnum = container.GetNestedType("ByteEnum", BindingFlags.Public ||| BindingFlags.NonPublic)
212+
Assert.NotNull(byteEnum)
213+
Assert.True(byteEnum.IsEnum, "ByteEnum should be recognised as an enum via target context")
214+
// GetEnumUnderlyingType on a TargetTypeDefinition reads the "value__" field type from IL.
215+
Assert.Equal("System.Byte", byteEnum.GetEnumUnderlyingType().FullName)
216+
217+
[<Fact>]
218+
let ``Int64 enum underlying type is correct when read via target context (ReadRelatedAssembly)``() =
219+
let runtimeAssemblyRefs = Targets.DotNetStandard20FSharpRefs()
220+
let runtimeAssembly = runtimeAssemblyRefs.[0]
221+
let cfg = Testing.MakeSimulatedTypeProviderConfig (__SOURCE_DIRECTORY__, runtimeAssembly, runtimeAssemblyRefs)
222+
let tp = GenerativeInt64EnumsProvider(cfg) :> TypeProviderForNamespaces
223+
let targetAssem = makeTargetAssembly tp
224+
225+
let container = targetAssem.GetType("Int64Enums.Provided.Container")
226+
Assert.NotNull(container)
227+
228+
let int64Enum = container.GetNestedType("Int64Enum", BindingFlags.Public ||| BindingFlags.NonPublic)
229+
Assert.NotNull(int64Enum)
230+
Assert.True(int64Enum.IsEnum, "Int64Enum should be recognised as an enum via target context")
231+
Assert.Equal("System.Int64", int64Enum.GetEnumUnderlyingType().FullName)

0 commit comments

Comments
 (0)