Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/Fable.Transforms/Python/Fable2Python.Annotation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 27 additions & 15 deletions src/Fable.Transforms/Python/Fable2Python.Transforms.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 37 additions & 0 deletions tests/Python/TestUnionType.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
[<Fable.Core.AttachMembers>]
[<RequireQualifiedAccess>]
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

[<Fact>]
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

[<Fact>]
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