Skip to content

Commit

Permalink
[JS/TS] Add Pojo attribute (#4036)
Browse files Browse the repository at this point in the history
* [JS/TS] PojoDefinedByConsArgs

* [JS/TS] Fix PojoDefinedByConsArgs

* Update CHANGELOG

* [JS/TS] Rename PojoDefinedByConsArgs attribute

* Compile Fable.Core for metadata with net6 and Debug mode

* chore: update changelog to match rename of the attribute

---------

Co-authored-by: Maxime Mangel <[email protected]>
  • Loading branch information
alfonsogarciacaro and MangelMaxime authored Feb 16, 2025
1 parent 5c95930 commit 9cb29b8
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 160 deletions.
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)

### Fixed

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)

### Fixed

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 @@ -3793,7 +3803,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 @@ -4224,11 +4234,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

0 comments on commit 9cb29b8

Please sign in to comment.