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

Allow "ref" as valid prop, cleanup of Invalid_arguments and improve coverage #848

Closed
wants to merge 15 commits into from
Closed
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
56 changes: 29 additions & 27 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
"homepage": "https://reasonml.github.io/reason-react/",
"devDependencies": {
"jest": "^26.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^19.0.0-beta-26f2496093-20240514",
"react-dom": "^19.0.0-beta-26f2496093-20240514",
"react-test-renderer": "^18.2.0"
},
"jest": {
Expand Down
93 changes: 36 additions & 57 deletions ppx/reason_react_ppx.ml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ let labelled str = Labelled str
let optional str = Optional str

module Binding = struct
(* Binding is the interface that the ppx uses to interact with the bindings.
Here we define the same APIs as the bindings but it generates Parsetree *)
(* Binding is the interface that the ppx relies on to interact with the react bindings.
Here we define the same APIs as the bindings but it generates Parsetree nodes *)
module ReactDOM = struct
let domProps ~applyLoc ~loc props =
Builder.pexp_apply ~loc:applyLoc
Expand All @@ -58,9 +58,6 @@ module Binding = struct
end

module React = struct
let null ~loc =
Builder.pexp_ident ~loc { loc; txt = Ldot (Lident "React", "null") }

let array ~loc children =
Builder.pexp_apply ~loc
(Builder.pexp_ident ~loc
Expand Down Expand Up @@ -92,18 +89,22 @@ let rec find_opt p = function
| [] -> None
| x :: l -> if p x then Some x else find_opt p l

let getLabel str =
let getLabelOrEmpty str =
match str with Optional str | Labelled str -> str | Nolabel -> ""

let getLabel str =
match str with Optional str | Labelled str -> Some str | Nolabel -> None

let optionIdent = Lident "option"

let constantString ~loc str =
Builder.pexp_constant ~loc (Pconst_string (str, Location.none, None))

let safeTypeFromValue valueStr =
let valueStr = getLabel valueStr in
match String.sub valueStr 0 1 with "_" -> "T" ^ valueStr | _ -> valueStr
[@@raises Invalid_argument]
match getLabel valueStr with
| Some valueStr when String.sub valueStr 0 1 = "_" -> ("T" ^ valueStr)
| Some valueStr -> valueStr
| None -> ""

let keyType loc = Builder.ptyp_constr ~loc { loc; txt = Lident "string" } []

Expand Down Expand Up @@ -218,14 +219,12 @@ let otherAttrsPure { attr_name = loc; _ } = loc.txt <> "react.component"
let hasAttrOnBinding { pvb_attributes; _ } =
find_opt hasAttr pvb_attributes <> None

(* Finds the name of the variable the binding is assigned to, otherwise raises
Invalid_argument *)
(* Finds the name of the variable the binding is assigned to, otherwise raises an error *)
let getFnName binding =
match binding with
| { pvb_pat = { ppat_desc = Ppat_var { txt; _ }; _ }; _ } -> txt
| _ ->
raise (Invalid_argument "react.component calls cannot be destructured.")
[@@raises Invalid_argument]
| { pvb_loc; _} ->
Location.raise_errorf ~loc:pvb_loc "[@react.component] cannot be used with a destructured binding. Please use it on a `let make = ...` binding instead."

let makeNewBinding binding expression newName =
match binding with
Expand All @@ -237,22 +236,17 @@ let makeNewBinding binding expression newName =
pvb_expr = expression;
pvb_attributes = [ merlinFocus ];
}
| _ ->
raise (Invalid_argument "react.component calls cannot be destructured.")
[@@raises Invalid_argument]
| { pvb_loc; _ } ->
Location.raise_errorf ~loc:pvb_loc "[@react.component] cannot be used with a destructured binding. Please use it on a `let make = ...` binding instead."

(* Lookup the value of `props` otherwise raise Invalid_argument error *)
let getPropsNameValue _acc (loc, exp) =
match (loc, exp) with
(* Lookup the value of `props` otherwise raise errorf *)
let getPropsNameValue _acc (loc, expr) =
match (loc, expr) with
| ( { txt = Lident "props"; _ },
{ pexp_desc = Pexp_ident { txt = Lident str; _ }; _ } ) ->
{ propsName = str }
| { txt; _ }, _ ->
raise
(Invalid_argument
("react.component only accepts props as an option, given: "
^ Longident.last_exn txt))
[@@raises Invalid_argument]
| { txt; loc }, _ ->
Location.raise_errorf ~loc "[@react.component] only accepts 'props' as a field, given: %s" (Longident.last_exn txt)

(* Lookup the `props` record or string as part of [@react.component] and store
the name for use when rewriting *)
Expand All @@ -278,12 +272,10 @@ let getPropsAttr payload =
}
:: _rest)) ->
{ propsName = "props" }
| Some (PStr ({ pstr_desc = Pstr_eval (_, _); _ } :: _rest)) ->
raise
(Invalid_argument
"react.component accepts a record config with props as an options.")
| Some (PStr ({ pstr_desc = Pstr_eval (_, _); pstr_loc; _ } :: _rest)) ->
Location.raise_errorf ~loc:pstr_loc
"[@react.component] accepts a record config with 'props' as a field."
| _ -> defaultProps
[@@raises Invalid_argument]

(* Plucks the label, loc, and type_ from an AST node *)
let pluckLabelDefaultLocType (label, default, _, _, loc, type_) =
Expand Down Expand Up @@ -364,7 +356,6 @@ let rec recursivelyMakeNamedArgsForExternal ~types_come_from_signature list args
| _label, Some type_, _ -> type_)
args)
| [] -> args
[@@raises Invalid_argument]

(* Build an AST node for the [@bs.obj] representing props for a component *)
let makePropsValue fnName ~types_come_from_signature loc
Expand Down Expand Up @@ -394,7 +385,6 @@ let makePropsValue fnName ~types_come_from_signature loc
];
pval_loc = loc;
}
[@@raises Invalid_argument]

(* Build an AST node representing an `external` with the definition of the
[@bs.obj] *)
Expand All @@ -407,7 +397,6 @@ let makePropsExternal fnName loc ~component_is_external
(makePropsValue ~types_come_from_signature:component_is_external fnName
loc namedArgListWithKeyAndRef propsType);
}
[@@raises Invalid_argument]

(* Build an AST node for the signature of the `external` definition *)
let makePropsExternalSig fnName loc namedArgListWithKeyAndRef propsType =
Expand All @@ -418,7 +407,6 @@ let makePropsExternalSig fnName loc namedArgListWithKeyAndRef propsType =
(makePropsValue ~types_come_from_signature:true fnName loc
namedArgListWithKeyAndRef propsType);
}
[@@raises Invalid_argument]

(* Build an AST node for the props name when converted to an object inside the
function signature *)
Expand Down Expand Up @@ -502,7 +490,6 @@ let makeExternalDecl fnName loc namedArgListWithKeyAndRef namedTypeList =
makePropsExternal ~component_is_external:false fnName loc
(List.map pluckLabelDefaultLocType namedArgListWithKeyAndRef)
(makePropsType ~loc namedTypeList)
[@@raises Invalid_argument]

(* TODO: some line number might still be wrong *)
let jsxMapper =
Expand All @@ -513,7 +500,7 @@ let jsxMapper =
let argsForMake = argsWithLabels in
let keyProps, otherProps =
List.partition
(fun (arg_label, _) -> "key" = getLabel arg_label)
(fun (arg_label, _) -> "key" = getLabelOrEmpty arg_label)
argsForMake
in
let jsxExpr, key, childrenProp =
Expand All @@ -527,10 +514,12 @@ let jsxMapper =
(label, mapper#expression ctxt expression))
in
let isCap str =
let first = String.sub str 0 1 [@@raises Invalid_argument] in
let capped = String.uppercase_ascii first in
first = capped
[@@raises Invalid_argument]
match String.length str with
| 0 -> false
| _ ->
let first = String.sub str 0 1 in
let capped = String.uppercase_ascii first in
first = capped
in
let ident =
match modulePath with
Expand Down Expand Up @@ -592,7 +581,7 @@ let jsxMapper =
let componentNameExpr = constantString ~loc:callerLoc id in
let keyProps, nonChildrenProps =
List.partition
(fun (arg_label, _) -> "key" = getLabel arg_label)
(fun (arg_label, _) -> "key" = getLabelOrEmpty arg_label)
nonChildrenProps
in

Expand Down Expand Up @@ -641,17 +630,9 @@ let jsxMapper =
let rec recursivelyTransformNamedArgsForMake ~ctxt mapper expr list =
let expr = mapper#expression ctxt expr in
match expr.pexp_desc with
(* TODO: make this show up with a loc. *)
| Pexp_fun (Labelled "key", _, _, _) | Pexp_fun (Optional "key", _, _, _) ->
raise
(Invalid_argument
"Key cannot be accessed inside of a component. Don't worry - you \
can always key a component from its parent!")
| Pexp_fun (Labelled "ref", _, _, _) | Pexp_fun (Optional "ref", _, _, _) ->
raise
(Invalid_argument
"Ref cannot be passed as a normal prop. Please use `forwardRef` \
API instead.")
Location.raise_errorf ~loc:expr.pexp_loc
("~key cannot be accessed from the component props. Please set the key where the component is being used.")
| Pexp_fun
( ((Optional label | Labelled label) as arg),
default,
Expand Down Expand Up @@ -698,7 +679,6 @@ let jsxMapper =
"reason-react-ppx: react.component refs only support plain arguments \
and type annotations."
| _ -> (list, None)
[@@raises Invalid_argument]
in

let argToType types (name, default, _noLabelName, _alias, loc, type_) =
Expand All @@ -720,7 +700,7 @@ let jsxMapper =
} )
:: types
| Some type_, name, Some _default ->
( getLabel name,
( getLabelOrEmpty name,
[],
{
ptyp_desc = Ptyp_constr ({ loc; txt = optionIdent }, [ type_ ]);
Expand All @@ -729,7 +709,7 @@ let jsxMapper =
ptyp_attributes = [];
} )
:: types
| Some type_, name, _ -> (getLabel name, [], type_) :: types
| Some type_, name, _ -> (getLabelOrEmpty name, [], type_) :: types
| None, Optional label, _ ->
( label,
[],
Expand Down Expand Up @@ -761,7 +741,6 @@ let jsxMapper =
} )
:: types
| _ -> types
[@@raises Invalid_argument]
in

let argToConcreteType types (name, loc, type_) =
Expand Down Expand Up @@ -1094,7 +1073,7 @@ let jsxMapper =
in
let pluckArg (label, _, _, alias, loc, _) =
( label,
match getLabel label with
match getLabelOrEmpty label with
| "" -> Builder.pexp_ident ~loc { txt = Lident alias; loc }
| labelString ->
Builder.pexp_apply ~loc
Expand Down
17 changes: 17 additions & 0 deletions ppx/test/component-without-make.t/input.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Component_with_x_as_main_function = {
[@react.component]
let x = () => <div />;
};

module Component_with_createElement_as_main_function = {
[@react.component]
let createElement = (~lola) => <div> {React.string(lola)} </div>;
};

module Parent_rendering = {
[@react.component]
let make = () => {
<Component_with_x_as_main_function.x />;
<Component_with_createElement_as_main_function lola="lola" />;
};
};
Loading
Loading