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
1 change: 1 addition & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

* [JS/TS/Python] Fix record/struct types augmented with `static let` or `static member val` generating extra constructor parameters for each static field, causing constructor arguments to be assigned to wrong slots (by @MangelMaxime)
* [TS] Annotate `System.Collections.Generic.IList<T>` as `MutableArray<T>` (by @MangelMaxime)
* [JS/TS] Fix `ResizeArray` index getter/setter not throwing `IndexOutOfRangeException` when index is out of bounds (fix #3812) (by @MangelMaxime)
* [Beam] Fix unused term warning in try/catch when exception variable is not referenced (by @dbrattli)
Expand Down
1 change: 1 addition & 0 deletions src/Fable.Compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

* [JS/TS/Python] Fix record/struct types augmented with `static let` or `static member val` generating extra constructor parameters for each static field, causing constructor arguments to be assigned to wrong slots (by @MangelMaxime)
* [TS] Annotate `System.Collections.Generic.IList<T>` as `MutableArray<T>` (by @MangelMaxime)
* [JS/TS] Fix `ResizeArray` index getter/setter not throwing `IndexOutOfRangeException` when index is out of bounds (fix #3812) (by @MangelMaxime)
* [Beam] Fix unused term warning in try/catch when exception variable is not referenced (by @dbrattli)
Expand Down
23 changes: 15 additions & 8 deletions src/Fable.Transforms/Fable2Babel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,15 @@ module Reflection =

let fields =
ent.FSharpFields
|> List.map (fun fi ->
let fieldName = sanitizeMemberName fi.Name |> Expression.stringLiteral
|> List.choose (fun fi ->
if fi.IsStatic then
None
else
let fieldName = sanitizeMemberName fi.Name |> Expression.stringLiteral

let typeInfo = transformTypeInfoFor Reflection com ctx r genMap fi.FieldType
let typeInfo = transformTypeInfoFor Reflection com ctx r genMap fi.FieldType

Expression.arrayExpression ([| fieldName; typeInfo |])
Some(Expression.arrayExpression ([| fieldName; typeInfo |]))
)
|> List.toArray

Expand Down Expand Up @@ -3410,10 +3413,13 @@ but thanks to the optimisation done below we get

let getEntityFieldsAsIdents (ent: Fable.Entity) =
ent.FSharpFields
|> List.map (fun field ->
let name = sanitizeName field.Name
let typ = field.FieldType
{ makeTypedIdent typ name with IsMutable = field.IsMutable }
|> List.choose (fun field ->
if field.IsStatic then
None
else
let name = sanitizeName field.Name
let typ = field.FieldType
Some { makeTypedIdent typ name with IsMutable = field.IsMutable }
)

let declareClassWithParams
Expand Down Expand Up @@ -3997,6 +4003,7 @@ but thanks to the optimisation done below we get
yield callSuperAsStatement []
yield!
ent.FSharpFields
|> List.filter (fun field -> not field.IsStatic)
|> List.mapi (fun i field ->
let left = get None thisExpr field.Name

Expand Down
6 changes: 5 additions & 1 deletion src/Fable.Transforms/FableTransforms.fs
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,11 @@ module private Transforms =
Emit({ emitInfo with CallInfo = { callInfo with Args = args } }, t, r)
// Uncurry also values in setters or new record/union/tuple
| Value(NewRecord(args, ent, genArgs), r) ->
let args = com.GetEntity(ent).FSharpFields |> uncurryConsArgs args
let args =
com.GetEntity(ent).FSharpFields
|> Seq.filter (fun f -> not f.IsStatic)
|> uncurryConsArgs args

Value(NewRecord(args, ent, genArgs), r)
| Value(NewAnonymousRecord(args, fieldNames, genArgs, isStruct), r) ->
let args = uncurryArgs com false genArgs args
Expand Down
19 changes: 11 additions & 8 deletions src/Fable.Transforms/Python/Fable2Python.Reflection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,19 @@ let private transformRecordReflectionInfo com ctx r (ent: Fable.Entity) generics

let fields, stmts =
ent.FSharpFields
|> Seq.map (fun fi ->
let typeInfo, stmts = transformTypeInfo com ctx r genMap fi.FieldType
|> Seq.choose (fun fi ->
if fi.IsStatic then
None
else
let typeInfo, stmts = transformTypeInfo com ctx r genMap fi.FieldType

let name =
if Util.shouldUseRecordFieldNaming ent then
fi.Name |> Naming.toRecordFieldSnakeCase |> Helpers.clean
else
fi.Name |> Naming.toSnakeCase |> Helpers.clean
let name =
if Util.shouldUseRecordFieldNaming ent then
fi.Name |> Naming.toRecordFieldSnakeCase |> Helpers.clean
else
fi.Name |> Naming.toSnakeCase |> Helpers.clean

Expression.tuple [ Expression.stringConstant name; typeInfo ], stmts
Some(Expression.tuple [ Expression.stringConstant name; typeInfo ], stmts)
)
|> Seq.toList
|> Helpers.unzipArgs
Expand Down
45 changes: 38 additions & 7 deletions src/Fable.Transforms/Python/Fable2Python.Transforms.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3197,14 +3197,17 @@ let getEntityFieldsAsIdents (com: IPythonCompiler) (ent: Fable.Entity) =
Naming.toPythonNaming

ent.FSharpFields
|> Seq.map (fun field ->
let name =
(entityNamingConvention field.Name, Naming.NoMemberPart)
||> Naming.sanitizeIdent (fun _ -> false)
|> Seq.choose (fun field ->
if field.IsStatic then
None
else
let name =
(entityNamingConvention field.Name, Naming.NoMemberPart)
||> Naming.sanitizeIdent (fun _ -> false)

let typ = field.FieldType
let typ = field.FieldType

{ makeTypedIdent typ name with IsMutable = field.IsMutable }
Some { makeTypedIdent typ name with IsMutable = field.IsMutable }
)
|> Seq.toArray

Expand Down Expand Up @@ -3248,6 +3251,7 @@ let declareDataClassType
// lambda types in the type annotations to match.
let props =
ent.FSharpFields
|> List.filter (fun field -> not field.IsStatic)
|> List.mapi (fun i field ->
// Get the argument name from consArgs (preserves the Python naming convention)
let argName =
Expand Down Expand Up @@ -3343,8 +3347,33 @@ let declareDataClassType
returns = Expression.name "int"
)

// Generate ClassVar annotations for static fields so that Pyright recognizes
// the class-level attributes assigned by the static constructor (_cctor).
// With slots=True, unannotated class attributes are not allowed.
let staticFieldAnnotations =
ent.FSharpFields
|> List.choose (fun field ->
if
field.IsStatic
&& not (field.Name.StartsWith("init@", System.StringComparison.Ordinal))
then
let fieldName = field.Name.TrimEnd('@') |> Naming.toPythonNaming
let ta, _ = Annotation.typeAnnotation com ctx None field.FieldType
let classVar = com.GetImportExpr(ctx, "typing", "ClassVar")
let classVarAnnotation = Expression.subscript (classVar, ta)
Some(Statement.annAssign (Expression.name fieldName, annotation = classVarAnnotation))
else
None
)

let classBody =
let body = [ yield! props; yield! classMembers; yield hashMethod ]
let body =
[
yield! staticFieldAnnotations
yield! props
yield! classMembers
yield hashMethod
]

match body with
| [] -> [ Statement.ellipsis ]
Expand Down Expand Up @@ -4142,6 +4171,7 @@ let transformClassWithCompilerGeneratedConstructor
if classAttributes.Style = ClassStyle.Attributes then
yield!
ent.FSharpFields
|> List.filter (fun field -> not field.IsStatic)
|> List.collect (fun field ->
let fieldName = field.Name |> Naming.toPropertyNaming
let left = get com ctx None thisExpr fieldName false
Expand All @@ -4155,6 +4185,7 @@ let transformClassWithCompilerGeneratedConstructor
else
yield!
ent.FSharpFields
|> List.filter (fun field -> not field.IsStatic)
|> List.collecti (fun i field ->
let fieldName =
if shouldUseRecordFieldNaming ent then
Expand Down
25 changes: 25 additions & 0 deletions tests/Js/Main/RecordTypeTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ type Time =
type CarInterior = { Seats: int }
type Car = { Interior: CarInterior }

// Issue #3923 - static let on a record shifts constructor arg indices
type RecordWithStaticLet =
{ id: int }
static let _a = 2
static member a = _a

// Issue #4091 - static member val on a record causes shared mutable state
type RecordWithStaticMemberVal =
{ xs: ResizeArray<int> }
static member val empty = { xs = ResizeArray() }

let tests =
testList "RecordTypes" [

Expand Down Expand Up @@ -160,4 +171,18 @@ let tests =
{| car with Interior.Seats = 5 |}
equal 5 car2.Interior.Seats

// Issue #3923
testCase "Record with static let has correct constructor" <| fun () ->
let x = { id = 1 }
equal 1 x.id
equal 2 RecordWithStaticLet.a

// Issue #4091
testCase "Record with static member val does not share state across instances" <| fun () ->
let a = RecordWithStaticMemberVal.empty
let b = RecordWithStaticMemberVal.empty
a.xs.Add 1
equal 1 a.xs.Count
equal 1 b.xs.Count

]
29 changes: 28 additions & 1 deletion tests/Python/TestRecordType.fs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ type Time =
type CarInterior = { Seats: int }
type Car = { Interior: CarInterior }

// Issue #3923 - static let on a record shifts constructor arg indices
type RecordWithStaticLet =
{ id: int }
static let _a = 2
static member a = _a

// Issue #4091 - static member val on a record causes shared mutable state
type RecordWithStaticMemberVal =
{ xs: ResizeArray<int> }
static member val empty = { xs = ResizeArray() }

type RecordA =
{ OptionalField : string option }

Expand Down Expand Up @@ -199,4 +210,20 @@ type RecordWithProperty =
[<Fact>]
let ``test Record property access uses correct naming`` () =
let x = { items = ["Hello"; "World"] }
equal "Hello - World" x.fullName
equal "Hello - World" x.fullName

// Issue #3923
[<Fact>]
let ``test Record with static let has correct constructor`` () =
let x = { id = 1 }
equal 1 x.id
equal 2 RecordWithStaticLet.a

// Issue #4091
[<Fact>]
let ``test Record with static member val does not share state across instances`` () =
let a = RecordWithStaticMemberVal.empty
let b = RecordWithStaticMemberVal.empty
a.xs.Add 1
equal 1 a.xs.Count
equal 1 b.xs.Count
Loading