diff --git a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs index 17a73dacc3..3650625fde 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Annotation.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Annotation.fs @@ -404,6 +404,33 @@ let resolveGenerics com ctx generics repeatedGenerics : Expression list * Statem |> List.map (typeAnnotation com ctx repeatedGenerics) |> Helpers.unzipArgs +/// Serialize an annotation expression to its Python source form for use as a string forward +/// reference (e.g. `Array[Example] | None` -> "Array[Example] | None"). Only the shapes +/// `typeAnnotation` can produce need handling. +let rec annotationToString (expr: Expression) = + match expr with + | Expression.Name { Id = Identifier id } -> id + | Expression.Attribute { + Value = value + Attr = Identifier attr + } -> $"{annotationToString value}.{attr}" + | Expression.Subscript { + Value = value + Slice = slice + } -> $"{annotationToString value}[{annotationToString slice}]" + | Expression.Tuple { Elements = elements } -> elements |> List.map annotationToString |> String.concat ", " + | Expression.List(elements, _) -> + let inner = elements |> List.map annotationToString |> String.concat ", " + $"[{inner}]" + | Expression.BinOp { + Left = left + Right = right + Operator = BitOr + } -> $"{annotationToString left} | {annotationToString right}" + | Expression.Constant(StringLiteral s, _) -> s + | Expression.Constant(NoneLiteral, _) -> "None" + | _ -> "Any" + let rec typeAnnotation (com: IPythonCompiler) ctx diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 8363698ca3..03c7cd96f8 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -4761,24 +4761,36 @@ let transformStaticProperty let staticPropertyClass = libValue com ctx "util" className - // Check if the property type references the current class (forward reference needed) + // `StaticProperty[T]` is a value expression evaluated in the class body, so a type + // referencing the enclosing entity (not yet bound there) raises `NameError`. Quoting just + // the name is not enough — an operator like the `|` in `Example | None` still runs eagerly + // and fails on a string operand. So emit the whole annotation as one string forward + // reference: it never evaluates at runtime, yet the type checker resolves it at module scope. + let referencesEnclosingEntity (propType: Fable.Type) = + // Walk the type structurally via `Type.Generics`, which enumerates the generic args of + // every case (Array, Option, List, Tuple, Lambda/Delegate, Nullable, anonymous records, + // nested declared types), so no shape is missed as new cases are added. + let rec check (t: Fable.Type) = + (match t with + | Fable.DeclaredType(entRef, _) -> (com.GetEntity entRef).FullName = ent.FullName + | _ -> false) + || List.exists check t.Generics + + check propType + let typeAnnotation = - // The property type is self-referencing when it is the enclosing entity. Compare by - // FullName as well as DisplayName: for unions the descriptor lives on the base class - // and the type alias (`name`) is only defined afterwards, so a bare name would raise - // NameError at class-body evaluation. - let isSelfReference = - match propType with - | Fable.DeclaredType(entRef, _) -> - let e = com.GetEntity(entRef) - e.DisplayName = name || e.FullName = ent.FullName - | _ -> false + if referencesEnclosingEntity propType then + // Render the enclosing union with its base-class name (`_Example`, not the alias + // `Example`): both resolve at module scope inside the forward-reference string, but + // the generated *value* is typed against the base class (e.g. a `unit -> Example` + // factory produces `() -> _Example`), so the base name keeps the annotation and the + // value consistent for the type checker. `typeAnnotation` still registers the + // imports the string references (e.g. `Array`) as a side effect. + let ctx = { ctx with EnclosingUnionBaseClass = Some ent.DisplayName } - if isSelfReference then - // Use string forward reference for self-referencing types - Expression.stringConstant name + let ta, _ = Annotation.typeAnnotation com ctx None propType + Expression.stringConstant (Annotation.annotationToString ta) else - // Use normal type annotation let ta, _ = Annotation.typeAnnotation com ctx None propType ta diff --git a/tests/Python/TestUnionType.fs b/tests/Python/TestUnionType.fs index 2336d44c52..f9b3d3f7bc 100644 --- a/tests/Python/TestUnionType.fs +++ b/tests/Python/TestUnionType.fs @@ -235,3 +235,40 @@ let ``test Union cases with no fields are physically equal`` () = obj.ReferenceEquals(MyUnion.Case0, MyUnion.Case0) |> equal true obj.ReferenceEquals(MyUnion.Case1 "a", MyUnion.Case1 "a") |> equal false obj.ReferenceEquals(T1, T1) |> equal true + +// See https://github.com/fable-compiler/Fable/issues/4736 +// A static member whose type references the enclosing union used to emit a class-body +// `StaticLazyProperty[...]` subscript that evaluated the union type before its name was bound, +// raising `NameError` at import time. The reference can be nested in any type shape, so each +// member below exercises a different one: array, option (rendered with `|`), nested list/array, +// tuple, and a function type (`unit -> Example`). +[] +[] +type Example = + | First + | Second + | Other of string + + static member All = [| Example.First; Example.Second |] + static member Head = Some Example.First + static member Nested = [ [| Example.First |] ] + static member Pair = (Example.First, Example.Second) + static member Factory: unit -> Example = fun () -> Example.First + +[] +let ``test Static union member returning the union type works`` () = + Example.All |> Array.length |> equal 2 + Example.All.[0] |> equal Example.First + Example.All.[1] |> equal Example.Second + +[] +let ``test Static union member with nested self-reference works`` () = + // Option (`_Example | None`): a bare quoted leaf would leave the `|` to run eagerly and fail. + Example.Head |> equal (Some Example.First) + // Nested list of array (`FSharpList[Array[_Example]]`). + Example.Nested |> List.head |> Array.head |> equal Example.First + // Tuple (`tuple[_Example, _Example]`). + Example.Pair |> fst |> equal Example.First + Example.Pair |> snd |> equal Example.Second + // Function type (`Callable[[], _Example]`) — missed by the original narrower detection. + Example.Factory() |> equal Example.First