Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JS/TS] PojoDefinedByConsArgs attribute #4036

Merged
merged 6 commits into from
Feb 16, 2025
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 @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [Python] - Print root module and module function comments (by @alfonsogarciacaro)
* [Rust] Add support for module comments (by @ncave)
* [Rust] Add support for null strings (by @ncave)
* [TS/JS] `Pojo` attribute support (by @alfonsogarciacaro)

## 5.0.0-alpha.9 - 2025-01-28

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 @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [Python] - Print root module and module function comments (by @alfonsogarciacaro)
* [Rust] Add support for module comments (by @ncave)
* [Rust] Add support for null strings (by @ncave)
* [TS/JS] `Pojo` attribute support (by @alfonsogarciacaro)

## 5.0.0-alpha.9 - 2025-01-28

Expand Down
4 changes: 4 additions & 0 deletions src/Fable.Core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

* [TS/JS] Add `Pojo` attribute (by @alfonsogarciacaro)

## 4.3.0 - 2024-01-25

### Added
Expand Down
37 changes: 26 additions & 11 deletions src/Fable.Core/Fable.Core.JS.fs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,32 @@ module JSX =
let nothing: Element = nativeOnly

module JS =
/// <summary>
/// Constructor calls to class decorated with this attribute will be compiled as JS pojos defined by the constructor arguments.
/// The class declaration will be erased, but in Typescript an interface will be generated.
/// </summary>
/// <example>
/// <code>
/// [&lt;Pojo&gt;]
/// type Person(name: string, ?age: int) =
/// member val name = name
/// member val age = age
///
/// let p = Person("Alice")
///
/// // Typescript
/// export interface Person {
/// name: string,
/// age?: number
/// }
///
/// export const p: Person = { name: "Alice" }
/// </code>
/// </example>
[<AttributeUsage(AttributeTargets.Class, AllowMultiple = false)>]
type PojoAttribute() =
inherit Attribute()

/// Used to remove the arguments of a surrounding function immediately calling a function decorated with this argument.
/// This is convenient to represent JS patterns when a function is actually loaded lazily with a dynamic import.
type RemoveSurroundingArgsAttribute() =
Expand Down Expand Up @@ -109,17 +135,6 @@ module JS =
/// that is representable as a Number value, which is approximately:
/// 2.2204460492503130808472633361816 x 10‍−‍16.
abstract EPSILON: float
/// <summary>
/// Returns true if passed value is finite.
/// Unlike the global isFinite, Number.isFinite doesn't forcibly convert the parameter to a
/// number. Only finite values of the type number, result in true.
/// </summary>
/// <param name="number">A numeric value.</param>
abstract isFinite: number: obj -> bool
/// <summary>Returns true if the value passed is an integer, false otherwise.</summary>
/// <param name="number">A numeric value.</param>
abstract isInteger: number: obj -> bool
/// <summary>
/// Returns a Boolean value that indicates whether a value is the reserved value NaN (not a
/// number). Unlike the global isNaN(), Number.isNaN() doesn't forcefully convert the parameter
/// to a number. Only values of the type number, that are also NaN, result in true.
Expand Down
4 changes: 2 additions & 2 deletions src/Fable.Transforms/Dart/Fable2Dart.fs
Original file line number Diff line number Diff line change
Expand Up @@ -461,8 +461,8 @@ module Util =
|> Option.map (splitNamedArgs args)
|> function
| None -> args, []
| Some(args, []) -> args, []
| Some(args, namedArgs) ->
| Some(args, None) -> args, []
| Some(args, Some namedArgs) ->
args,
namedArgs
|> List.choose (fun (p, v) ->
Expand Down
93 changes: 29 additions & 64 deletions src/Fable.Transforms/FSharp2Fable.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,6 @@ type FsMemberFunctionOrValue(m: FSharpMemberOrFunctionOrValue) =
member _.Attributes = m.Attributes |> Seq.map (fun x -> FsAtt(x) :> Fable.Attribute)

member _.CurriedParameterGroups =
let mutable i = -1

let namedParamsIndex =
m.Attributes
|> Helpers.tryFindAttrib Atts.paramObject
Expand All @@ -282,6 +280,13 @@ type FsMemberFunctionOrValue(m: FSharpMemberOrFunctionOrValue) =
| Some(_, (:? int as index)) -> index
| _ -> 0
)
|> Option.orElseWith (fun () ->
m.DeclaringEntity
|> Option.bind (fun ent -> ent.Attributes |> Helpers.tryFindAttrib Atts.pojoDefinedByConsArgs)
|> Option.map (fun _ -> 0)
)

let mutable i = -1

m.CurriedParameterGroups
|> Seq.mapToList (
Expand Down Expand Up @@ -821,46 +826,6 @@ module Helpers =
// [<Global>] attribute on the type and[<ParamObject>] on the constructors
// so we can transform it into an interface

/// <summary>
/// Check if the entity is decorated with the <code>Global</code> attribute
/// and all its constructors are decorated with <code>ParamObject</code> attribute.
///
/// This is used to identify classes that should be transformed into interfaces.
/// </summary>
/// <param name="entity"></param>
/// <returns>
/// <code>true</code> if the entity is a global type with all constructors as param objects,
/// <code>false</code> otherwise.
/// </returns>
let isParamObjectClassPattern (entity: Fable.Entity) =
let isGlobalType =
entity.Attributes |> Seq.exists (fun att -> att.Entity.FullName = Atts.global_)

let areAllConstructorsParamObject =
entity.MembersFunctionsAndValues
|> Seq.filter _.IsConstructor
|> Seq.forall (fun memb ->
// Empty constructors are considered valid as it allows to simplify unwraping
// complex Union types
//
// [<AllowNullLiteral>]
// [<Global>]
// type ClassWithUnion private () =
// [<ParamObjectAttribute; Emit("$0")>]
// new (stringOrNumber : string) = ClassWithUnion()
// [<ParamObjectAttribute; Emit("$0")>]
// new (stringOrNumber : int) = ClassWithUnion()
//
// Without this trick when we have a lot of U2, U3, etc. to map it is really difficult
// or verbose to craft the correct F# class. By using, an empty constructor we can
// "bypass" the F# type system.
memb.CurriedParameterGroups |> List.concat |> List.isEmpty
|| memb.Attributes
|> Seq.exists (fun att -> att.Entity.FullName = Atts.paramObject)
)

isGlobalType && areAllConstructorsParamObject

let tryPickAttrib attFullNames (attributes: FSharpAttribute seq) =
let attFullNames = Map attFullNames

Expand Down Expand Up @@ -2046,25 +2011,10 @@ module Util =

let tryGlobalOrImportedAttributes (com: Compiler) (entRef: Fable.EntityRef) (attributes: Fable.Attribute seq) =
let globalRef customName =
let name =
// Custom name has precedence
match customName with
| Some name -> name
| None ->
let entity = com.GetEntity(entRef)

// If we are generating TypeScript, and the entity is an object class pattern
// we need to use the compiled name, replacing '`' with '$' to mimic
// how Fable generates the compiled name for generic types
// I was not able to find where this is done in Fable, so I am doing it manually here
if com.Options.Language = TypeScript && isParamObjectClassPattern entity then
entity.CompiledName.Replace("`", "$")
// Otherwise, we use the display name as `Global` is often used to describe external API
// and we want to keep the original name
else
entRef.DisplayName

name |> makeTypedIdent Fable.Any |> Fable.IdentExpr |> Some
defaultArg customName entRef.DisplayName
|> makeTypedIdent Fable.Any
|> Fable.IdentExpr
|> Some

match attributes with
| _ when entRef.FullName.StartsWith("Fable.Core.JS.", StringComparison.Ordinal) -> globalRef None
Expand Down Expand Up @@ -2146,6 +2096,12 @@ module Util =
| _ -> false
))

let isPojoDefinedByConsArgsEntity (entity: Fable.Entity) =
entity |> hasAttribute Atts.pojoDefinedByConsArgs

let isPojoDefinedByConsArgsFSharpEntity (ent: FSharpEntity) =
ent.Attributes |> hasAttrib Atts.pojoDefinedByConsArgs

let isEmittedOrImportedMember (memb: FSharpMemberOrFunctionOrValue) =
memb.Attributes
|> Seq.exists (fun att ->
Expand Down Expand Up @@ -2405,7 +2361,12 @@ module Util =
// Don't mangle interfaces by default (for better interop) unless they have Mangle attribute
| _ when ent.IsInterface -> tryMangleAttribute ent.Attributes |> Option.defaultValue false
// Mangle members from abstract classes unless they are global/imported or with explicitly attached members
| _ -> not (isGlobalOrImportedFSharpEntity ent || isAttachMembersEntity com ent)
| _ ->
not (
isGlobalOrImportedFSharpEntity ent
|| isAttachMembersEntity com ent
|| isPojoDefinedByConsArgsFSharpEntity ent
)

let getMangledAbstractMemberName (ent: FSharpEntity) memberName overloadHash =
// TODO: Error if entity doesn't have fullName?
Expand Down Expand Up @@ -2681,8 +2642,12 @@ module Util =
let moduleOrClassExpr =
match tryGlobalOrImportedFSharpEntity com e with
| Some expr -> Some expr
// AttachMembers classes behave the same as global/imported classes
| None when not (com.Options.Language = Rust) && isAttachMembersEntity com e ->
// AttachMembers/Pojo classes behave
// the same as global/imported classes
| None when
com.Options.Language <> Rust
&& (isAttachMembersEntity com e || isPojoDefinedByConsArgsFSharpEntity e)
->
FsEnt.Ref e |> entityIdent com |> Some
| None -> None

Expand Down
5 changes: 2 additions & 3 deletions src/Fable.Transforms/FSharp2Fable.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1486,7 +1486,7 @@ let private isIgnoredNonAttachedMember (memb: FSharpMemberOrFunctionOrValue) =
)
|| (
match memb.DeclaringEntity with
| Some ent -> isGlobalOrImportedFSharpEntity ent
| Some ent -> isGlobalOrImportedFSharpEntity ent || isPojoDefinedByConsArgsFSharpEntity ent
| None -> false
)

Expand Down Expand Up @@ -2044,8 +2044,7 @@ let rec private transformDeclarations (com: FableCompiler) ctx fsDecls =

if
(isErasedOrStringEnumEntity ent && Compiler.Language <> TypeScript)
|| (isGlobalOrImportedEntity ent
&& (not (Compiler.Language = TypeScript && isParamObjectClassPattern ent)))
|| isGlobalOrImportedEntity ent
then
[]
else
Expand Down
80 changes: 45 additions & 35 deletions src/Fable.Transforms/Fable2Babel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1878,7 +1878,7 @@ module Util =

Expression.newExpression (classExpr, [||])

let transformCallArgs
let transformCallArgsWithNamedArgs
(com: IBabelCompiler)
ctx
(callInfo: Fable.CallInfo)
Expand Down Expand Up @@ -1906,26 +1906,12 @@ module Util =
paramsInfo
|> Option.map (splitNamedArgs args)
|> function
| None -> args, None
| Some(args, []) ->
// Detect if the method has a ParamObject attribute
// If yes and no argument is passed, pass an empty object
// See https://github.com/fable-compiler/Fable/issues/3480
match callInfo.MemberRef with
| Some(Fable.MemberRef(_, info)) ->
let hasParamObjectAttribute =
info.AttributeFullNames
|> List.tryFind (fun attr -> attr = Atts.paramObject)
|> Option.isSome

if hasParamObjectAttribute then
args, Some(makeJsObject [])
else
args, None
| _ ->
// Here detect empty named args
args, None
| Some(args, namedArgs) ->
| None
| Some(_, None) -> args, None
// If there are named arguments but none is passed, pass an empty object
// See https://github.com/fable-compiler/Fable/issues/3480
| Some(args, Some []) -> args, Some(makeJsObject [])
| Some(args, Some namedArgs) ->
let objArg =
namedArgs
|> List.choose (fun (p, v) ->
Expand Down Expand Up @@ -1958,9 +1944,12 @@ module Util =
else
List.map (fun e -> com.TransformAsExpr(ctx, e)) args

match objArg with
| None -> args
| Some objArg -> args @ [ objArg ]
args, objArg

let transformCallArgs com ctx callInfo memberInfo =
match transformCallArgsWithNamedArgs com ctx callInfo memberInfo with
| args, None -> args
| args, Some objArg -> args @ [ objArg ]

let resolveExpr t strategy babelExpr : Statement =
match strategy with
Expand Down Expand Up @@ -2146,16 +2135,37 @@ module Util =
transformJsxCall com ctx callee callInfo.Args memberInfo
| memberInfo ->
let callee = com.TransformAsExpr(ctx, callee)
let args = transformCallArgs com ctx callInfo memberInfo

let nonNamedArgs, namedArgs =
transformCallArgsWithNamedArgs com ctx callInfo memberInfo

let args = nonNamedArgs @ Option.toList namedArgs

match callInfo.ThisArg with
| None when List.contains "new" callInfo.Tags ->
let typeParamInst =
match typ with
| Fable.DeclaredType(_entRef, genArgs) -> makeTypeParamInstantiationIfTypeScript com ctx genArgs
let pojo =
match nonNamedArgs, namedArgs, memberInfo with
| [], Some namedArgs, Some memberInfo ->
match memberInfo.DeclaringEntity |> Option.bind com.TryGetEntity with
| Some e when FSharp2Fable.Util.isPojoDefinedByConsArgsEntity e -> Some namedArgs
| _ -> None
| _ -> None

Expression.newExpression (callee, List.toArray args, ?typeArguments = typeParamInst, ?loc = range)
match pojo with
| Some pojo -> pojo
| None ->
let typeParamInst =
match typ with
| Fable.DeclaredType(_entRef, genArgs) ->
makeTypeParamInstantiationIfTypeScript com ctx genArgs
| _ -> None

Expression.newExpression (
callee,
List.toArray args,
?typeArguments = typeParamInst,
?loc = range
)
| None -> callFunction com ctx range callee callInfo.GenericArgs args
| Some(TransformExpr com ctx thisArg) ->
callFunction com ctx range callee callInfo.GenericArgs (thisArg :: args)
Expand Down Expand Up @@ -3783,7 +3793,7 @@ module Util =

declareType com ctx ent decl.Name args body baseExpr classMembers

let transformParamObjectClassPatternToInterface
let transformPojoDefinedByConsArgsToInterface
(com: IBabelCompiler)
ctx
(ent: Fable.Entity)
Expand Down Expand Up @@ -4214,11 +4224,11 @@ module Util =
else
[]
| ent ->
if
Compiler.Language = TypeScript
&& FSharp2Fable.Helpers.isParamObjectClassPattern ent
then
transformParamObjectClassPatternToInterface com ctx ent decl
if FSharp2Fable.Util.isPojoDefinedByConsArgsEntity ent then
if Compiler.Language = TypeScript then
transformPojoDefinedByConsArgsToInterface com ctx ent decl
else
[]
else

let classMembers =
Expand Down
4 changes: 2 additions & 2 deletions src/Fable.Transforms/Python/Fable2Python.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1972,8 +1972,8 @@ module Util =
|> Option.map (splitNamedArgs args)
|> function
| None -> args, None, []
| Some(args, []) -> args, None, []
| Some(args, namedArgs) ->
| Some(args, None) -> args, None, []
| Some(args, Some namedArgs) ->
let objArg, stmts =
namedArgs
|> List.choose (fun (p, v) ->
Expand Down
Loading
Loading