diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index f700a91832..e8b7067401 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -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` as `MutableArray` (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) diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 41339e79c3..39130296c5 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -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` as `MutableArray` (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) diff --git a/src/Fable.Transforms/Fable2Babel.fs b/src/Fable.Transforms/Fable2Babel.fs index 7b56ea98ac..7433a19e51 100644 --- a/src/Fable.Transforms/Fable2Babel.fs +++ b/src/Fable.Transforms/Fable2Babel.fs @@ -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 @@ -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 @@ -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 diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index 0504a57575..ea5a437069 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -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 diff --git a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs index 5c99228a47..205eb35f25 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs @@ -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 diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 7f280d15ec..e4d791e7b1 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -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 @@ -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 = @@ -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 ] @@ -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 @@ -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 diff --git a/tests/Js/Main/RecordTypeTests.fs b/tests/Js/Main/RecordTypeTests.fs index 3a5f7ebee6..13d52494f2 100644 --- a/tests/Js/Main/RecordTypeTests.fs +++ b/tests/Js/Main/RecordTypeTests.fs @@ -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 } + static member val empty = { xs = ResizeArray() } + let tests = testList "RecordTypes" [ @@ -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 + ] diff --git a/tests/Python/TestRecordType.fs b/tests/Python/TestRecordType.fs index ed4cdca066..df1c867157 100644 --- a/tests/Python/TestRecordType.fs +++ b/tests/Python/TestRecordType.fs @@ -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 } + static member val empty = { xs = ResizeArray() } + type RecordA = { OptionalField : string option } @@ -199,4 +210,20 @@ type RecordWithProperty = [] let ``test Record property access uses correct naming`` () = let x = { items = ["Hello"; "World"] } - equal "Hello - World" x.fullName \ No newline at end of file + equal "Hello - World" x.fullName + +// Issue #3923 +[] +let ``test Record with static let has correct constructor`` () = + let x = { id = 1 } + equal 1 x.id + equal 2 RecordWithStaticLet.a + +// Issue #4091 +[] +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 \ No newline at end of file