diff --git a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs index 205eb35f2..e62c6343e 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs @@ -40,7 +40,7 @@ let private transformRecordReflectionInfo com ctx r (ent: Fable.Entity) generics let name = if Util.shouldUseRecordFieldNaming ent then - fi.Name |> Naming.toRecordFieldSnakeCase |> Helpers.clean + fi.Name |> Naming.toFieldSnakeCase |> Helpers.clean else fi.Name |> Naming.toSnakeCase |> Helpers.clean diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index d400f59cb..53d121e60 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -3223,7 +3223,7 @@ let getUnionFieldsAsIdents (_com: IPythonCompiler) _ctx (_ent: Fable.Entity) = let getEntityFieldsAsIdents (com: IPythonCompiler) (ent: Fable.Entity) = let entityNamingConvention = if shouldUseRecordFieldNaming ent then - Naming.toRecordFieldSnakeCase + Naming.toFieldSnakeCase else Naming.toPythonNaming @@ -3248,7 +3248,7 @@ let getEntityFieldsAsProps (com: IPythonCompiler) ctx (ent: Fable.Entity) = else let namingConvention = if shouldUseRecordFieldNaming ent then - Naming.toRecordFieldSnakeCase + Naming.toFieldSnakeCase else Naming.toPythonNaming @@ -3290,7 +3290,7 @@ let declareDataClassType consArgs.Args.[i].Arg else // Fallback to field name if consArgs doesn't have enough args - com.GetIdentifier(ctx, field.Name |> Naming.toRecordFieldSnakeCase |> Helpers.clean) + com.GetIdentifier(ctx, field.Name |> Naming.toFieldSnakeCase |> Helpers.clean) // Uncurry lambda types for field annotations since fields store uncurried functions let fieldType = @@ -3943,7 +3943,7 @@ let transformAttachedProperty // Apply the same naming convention as record fields for record types let propertyName = //if shouldUseRecordFieldNaming ent then - // memb.Name |> Naming.toRecordFieldSnakeCase |> Helpers.clean + // memb.Name |> Naming.toFieldSnakeCase |> Helpers.clean //else memb.Name |> Naming.toPropertyNaming @@ -4097,9 +4097,10 @@ let transformUnion (com: IPythonCompiler) ctx (ent: Fable.Entity) (entName: stri let fieldAnnotations = uci.UnionCaseFields |> List.map (fun field -> - // Convert to snake_case and clean to remove invalid characters like apostrophes - // Handles: "Item" -> "item", "Item1" -> "item1", "MyField" -> "my_field" - let fieldName = field.Name |> Naming.toSnakeCase |> Helpers.clean + // toFieldSnakeCase appends '_' to camelCase names ("name" -> "name_") so a + // field can't collide with the inherited `Union.name` property, which @dataclass + // would treat as a default value ("non-default argument follows default"). See #4645. + let fieldName = field.Name |> Naming.toFieldSnakeCase |> Helpers.clean // Uncurry lambda types for field annotations since union case fields // store uncurried functions at runtime (same as record fields) let fieldType = @@ -4238,7 +4239,7 @@ let transformClassWithCompilerGeneratedConstructor |> List.collecti (fun i field -> let fieldName = if shouldUseRecordFieldNaming ent then - field.Name |> Naming.toRecordFieldSnakeCase |> Helpers.clean + field.Name |> Naming.toFieldSnakeCase |> Helpers.clean else match Util.getFieldNamingKind com field.FieldType field.Name with | InstancePropertyBacking -> field.Name |> Naming.toPropertyBackingFieldNaming @@ -4680,7 +4681,7 @@ let transformStaticProperty // printfn "transformStaticProperty: %A" propName let propertyName = if shouldUseRecordFieldNaming ent then - propName |> Naming.toRecordFieldSnakeCase |> Helpers.clean + propName |> Naming.toFieldSnakeCase |> Helpers.clean else propName |> Naming.toPropertyNaming diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index 676f00037..97cc829ab 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -463,13 +463,13 @@ module Util = name - /// Determines if we should use the special record field naming convention (toRecordFieldSnakeCase) + /// Determines if we should use the special field naming convention (toFieldSnakeCase) /// for the given entity. Returns true for user-defined F# records, false for built-in types. let shouldUseRecordFieldNaming (ent: Fable.Entity) = ent.IsFSharpRecord && not (ent.FullName.StartsWith("Microsoft.FSharp.Core", StringComparison.Ordinal)) - /// Determines if we should use the special record field naming convention (toRecordFieldSnakeCase) + /// Determines if we should use the special field naming convention (toFieldSnakeCase) /// for the given entity reference. Returns true for user-defined F# records, false for built-in types. let shouldUseRecordFieldNamingForRef (entityRef: Fable.EntityRef) (ent: Fable.Entity) = ent.IsFSharpRecord @@ -510,7 +510,7 @@ module Util = | Fable.DeclaredType(entityRef, _) -> match com.TryGetEntity entityRef with | Some ent when shouldUseRecordFieldNamingForRef entityRef ent && isRecordField ent fieldName -> - fieldName |> Naming.toRecordFieldSnakeCase |> Helpers.clean + fieldName |> Naming.toFieldSnakeCase |> Helpers.clean | _ -> fieldName |> Naming.toPythonNaming // Fallback to Python naming for other types | _ -> fieldName |> Naming.toPropertyNaming diff --git a/src/Fable.Transforms/Python/Prelude.fs b/src/Fable.Transforms/Python/Prelude.fs index 1fc2332a3..d14f71f8a 100644 --- a/src/Fable.Transforms/Python/Prelude.fs +++ b/src/Fable.Transforms/Python/Prelude.fs @@ -33,10 +33,11 @@ module Naming = let toSnakeCase (name: string) = Naming.applyCaseRule CaseRules.SnakeCase name - /// Convert F# record field name to snake_case with special handling for camelCase/PascalCase conflicts. + /// Convert an F# field name (record or union case field) to snake_case with special handling + /// for camelCase/PascalCase conflicts. /// - If the name is PascalCase, convert to snake_case without suffix. /// - If the name is camelCase, convert to snake_case and add '_' suffix to avoid conflict with PascalCase. - let toRecordFieldSnakeCase (name: string) = + let toFieldSnakeCase (name: string) = let snakeCase = Naming.applyCaseRule CaseRules.SnakeCase name if name.Length > 0 && Char.IsLower(name.[0]) then diff --git a/tests/Python/TestUnionType.fs b/tests/Python/TestUnionType.fs index c8aa09712..ec6ab1c3f 100644 --- a/tests/Python/TestUnionType.fs +++ b/tests/Python/TestUnionType.fs @@ -211,3 +211,17 @@ type S = S of string let ``test sprintf formats strings cases correctly`` () = let s = sprintf "%A" (S "1") s |> equal "S \"1\"" + +// See https://github.com/fable-compiler/Fable/issues/4645 +// A named union case field called `name` collided with the inherited `Union.name` +// property, which Python's @dataclass treated as a default value. +type NamedFieldUnion = + | NamedCase of name: string * optional: string + +[] +let ``test Named union case fields work when a field is called name`` () = + let value = NamedCase("v1", "v2") + match value with + | NamedCase(name, optional) -> + name |> equal "v1" + optional |> equal "v2"