From 523ff1ecd5499d44a7c9f061883e6c82e44512aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cha=CC=81varri?= Date: Sun, 30 Aug 2020 19:38:50 +0200 Subject: [PATCH 1/8] Move React ppx from compiler repo, add tests React ppx copied from: https://github.com/rescript-lang/rescript-compiler/blob/4f4812aa4e1411029392f6dbcacc580e755d68d5/jscomp/syntax/reactjs_jsx_ppx.cppo.ml --- .depend | 6 +- Makefile | 5 +- src/reactjs_jsx_ppx.ml | 880 ++++++++++++++++++ src/reactjs_jsx_ppx.mli | 39 + src/res_cli.ml | 31 +- src/res_multi_printer.ml | 37 +- src/res_multi_printer.mli | 2 +- tests/api/resReactJsx.res | 6 + .../__snapshots__/render.spec.js.snap | 88 ++ tests/printer/reactJsx/commentAtTop.react.res | 6 + .../externalWithCustomName.react.res.fixme | 6 + tests/printer/reactJsx/innerModule.react.res | 16 + tests/printer/reactJsx/render.spec.js | 1 + tests/printer/reactJsx/topLevel.react.res | 5 + tests/res_test.ml | 25 + tests/runner.js | 50 +- 16 files changed, 1176 insertions(+), 27 deletions(-) create mode 100644 src/reactjs_jsx_ppx.ml create mode 100644 src/reactjs_jsx_ppx.mli create mode 100644 tests/api/resReactJsx.res create mode 100644 tests/printer/reactJsx/__snapshots__/render.spec.js.snap create mode 100644 tests/printer/reactJsx/commentAtTop.react.res create mode 100644 tests/printer/reactJsx/externalWithCustomName.react.res.fixme create mode 100644 tests/printer/reactJsx/innerModule.react.res create mode 100644 tests/printer/reactJsx/render.spec.js create mode 100644 tests/printer/reactJsx/topLevel.react.res diff --git a/.depend b/.depend index c2098132..ef2590a6 100644 --- a/.depend +++ b/.depend @@ -1,3 +1,5 @@ +src/reactjs_jsx_ppx.cmx : src/reactjs_jsx_ppx.cmi +src/reactjs_jsx_ppx.cmi : src/res_ast_conversion.cmx : src/res_ast_conversion.cmi src/res_ast_conversion.cmi : src/res_ast_debugger.cmx : src/res_driver.cmx src/res_doc.cmx \ @@ -6,7 +8,7 @@ src/res_ast_debugger.cmi : src/res_driver.cmi src/res_character_codes.cmx : src/res_cli.cmx : src/res_driver_reason_binary.cmx \ src/res_driver_ml_parser.cmx src/res_driver_binary.cmx src/res_driver.cmx \ - src/res_ast_debugger.cmx + src/res_ast_debugger.cmx src/reactjs_jsx_ppx.cmx src/res_comment.cmx : src/res_comment.cmi src/res_comment.cmi : src/res_comments_table.cmx : src/res_parsetree_viewer.cmx src/res_doc.cmx \ @@ -45,7 +47,7 @@ src/res_minibuffer.cmi : src/res_multi_printer.cmx : src/res_printer.cmx src/res_io.cmx \ src/res_driver_reason_binary.cmx src/res_driver_ml_parser.cmx \ src/res_driver.cmx src/res_diagnostics.cmx src/res_ast_conversion.cmx \ - src/res_multi_printer.cmi + src/reactjs_jsx_ppx.cmx src/res_multi_printer.cmi src/res_multi_printer.cmi : src/res_outcome_printer.cmx : src/res_token.cmx src/res_doc.cmx \ src/res_outcome_printer.cmi diff --git a/Makefile b/Makefile index 3c3f76eb..85941067 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ depend: $(OCAMLDEP) -native -I tests -I src src/*.ml src/*.mli tests/*.ml tests/*.mli > .depend API_FILES = \ + src/reactjs_jsx_ppx.cmx\ src/res_io.cmx\ src/res_minibuffer.cmx\ src/res_doc.cmx\ @@ -49,6 +50,8 @@ lib/rescript.exe: $(CLI_FILES) build-native: lib/refmt.exe lib/rescript.exe depend +build-ppx: src/reactjs_jsx_ppx.cmx + bootstrap: build-native ocaml unix.cma ./scripts/bspack.ml -bs-main Res_cli -I src -o ./lib/rescript.ml ./lib/rescript.exe -parse ml -print ns ./lib/Rescript.ml > ./lib/Rescript2.ml @@ -94,4 +97,4 @@ clean: rm -rf lib/rescript.exe rm -rf lib/test.exe git clean -dfx src -.PHONY: clean test roundtrip-test termination dce exception reanalyze bootstrap build-native +.PHONY: clean test roundtrip-test termination dce exception reanalyze bootstrap build-native build-ppx diff --git a/src/reactjs_jsx_ppx.ml b/src/reactjs_jsx_ppx.ml new file mode 100644 index 00000000..90459954 --- /dev/null +++ b/src/reactjs_jsx_ppx.ml @@ -0,0 +1,880 @@ +open Ast_helper +open Ast_mapper +open Asttypes +open Parsetree +open Longident + +let rec find_opt p = function [] -> None | x :: l -> if p x then Some x else find_opt p l + +let nolabel = Nolabel + +let labelled str = Labelled str + +let optional str = Optional str + +let isOptional str = match str with Optional _ -> true | _ -> false + +let isLabelled str = match str with Labelled _ -> true | _ -> false + +let getLabel str = match str with Optional str | Labelled str -> str | Nolabel -> "" + +let optionIdent = Lident "option" + +let constantString ~loc str = Ast_helper.Exp.constant ~loc (Pconst_string (str, None)) + +let safeTypeFromValue valueStr = + let valueStr = getLabel valueStr in + match String.sub valueStr 0 1 with "_" -> "T" ^ valueStr | _ -> valueStr + [@@raises Invalid_argument] + +let keyType loc = Typ.constr ~loc { loc; txt = optionIdent } [ Typ.constr ~loc { loc; txt = Lident "string" } [] ] + +type 'a children = ListLiteral of 'a | Exact of 'a + +type componentConfig = { propsName : string } + +(* if children is a list, convert it to an array while mapping each element. If not, just map over it, as usual *) +let transformChildrenIfListUpper ~loc ~mapper theList = + let rec transformChildren_ theList accum = + (* not in the sense of converting a list to an array; convert the AST + reprensentation of a list to the AST reprensentation of an array *) + match theList with + | { pexp_desc = Pexp_construct ({ txt = Lident "[]" }, None) } -> ( + match accum with + | [ singleElement ] -> Exact singleElement + | accum -> ListLiteral (Exp.array ~loc (List.rev accum)) ) + | { pexp_desc = Pexp_construct ({ txt = Lident "::" }, Some { pexp_desc = Pexp_tuple [ v; acc ] }) } -> + transformChildren_ acc (mapper.expr mapper v :: accum) + | notAList -> Exact (mapper.expr mapper notAList) + in + transformChildren_ theList [] + +let transformChildrenIfList ~loc ~mapper theList = + let rec transformChildren_ theList accum = + (* not in the sense of converting a list to an array; convert the AST + reprensentation of a list to the AST reprensentation of an array *) + match theList with + | { pexp_desc = Pexp_construct ({ txt = Lident "[]" }, None) } -> Exp.array ~loc (List.rev accum) + | { pexp_desc = Pexp_construct ({ txt = Lident "::" }, Some { pexp_desc = Pexp_tuple [ v; acc ] }) } -> + transformChildren_ acc (mapper.expr mapper v :: accum) + | notAList -> mapper.expr mapper notAList + in + transformChildren_ theList [] + +let extractChildren ?(removeLastPositionUnit = false) ~loc propsAndChildren = + let rec allButLast_ lst acc = + match lst with + | [] -> [] + | [ (Nolabel, { pexp_desc = Pexp_construct ({ txt = Lident "()" }, None) }) ] -> acc + | (Nolabel, _) :: _rest -> raise (Invalid_argument "JSX: found non-labelled argument before the last position") + | arg :: rest -> allButLast_ rest (arg :: acc) + [@@raises Invalid_argument] + in + let allButLast lst = allButLast_ lst [] |> List.rev [@@raises Invalid_argument] in + match List.partition (fun (label, _) -> label = labelled "children") propsAndChildren with + | [], props -> + (* no children provided? Place a placeholder list *) + (Exp.construct ~loc { loc; txt = Lident "[]" } None, if removeLastPositionUnit then allButLast props else props) + | [ (_, childrenExpr) ], props -> (childrenExpr, if removeLastPositionUnit then allButLast props else props) + | _ -> raise (Invalid_argument "JSX: somehow there's more than one `children` label") + [@@raises Invalid_argument] + +let unerasableIgnore loc = ({ loc; txt = "warning" }, PStr [ Str.eval (Exp.constant (Pconst_string ("-16", None))) ]) + +let merlinFocus = ({ loc = Location.none; txt = "merlin.focus" }, PStr []) + +(* Helper method to look up the [@react.component] attribute *) +let hasAttr (loc, _) = loc.txt = "react.component" + +(* Helper method to filter out any attribute that isn't [@react.component] *) +let otherAttrsPure (loc, _) = loc.txt <> "react.component" + +(* Iterate over the attributes and try to find the [@react.component] attribute *) +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 *) +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] + +let makeNewBinding binding expression newName = + match binding with + | { pvb_pat = { ppat_desc = Ppat_var ppat_var } as pvb_pat } -> + { + binding with + pvb_pat = { pvb_pat with ppat_desc = Ppat_var { ppat_var with txt = newName } }; + pvb_expr = expression; + pvb_attributes = [ merlinFocus ]; + } + | _ -> raise (Invalid_argument "react.component calls cannot be destructured.") + [@@raises Invalid_argument] + +(* Lookup the value of `props` otherwise raise Invalid_argument error *) +let getPropsNameValue _acc (loc, exp) = + match (loc, exp) 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 txt)) + [@@raises Invalid_argument] + +(* Lookup the `props` record or string as part of [@react.component] and store the name for use when rewriting *) +let getPropsAttr payload = + let defaultProps = { propsName = "Props" } in + match payload with + | Some (PStr ({ pstr_desc = Pstr_eval ({ pexp_desc = Pexp_record (recordFields, None) }, _) } :: _rest)) -> + List.fold_left getPropsNameValue defaultProps recordFields + | Some (PStr ({ pstr_desc = Pstr_eval ({ pexp_desc = Pexp_ident { txt = Lident "props" } }, _) } :: _rest)) -> + { propsName = "props" } + | Some (PStr ({ pstr_desc = Pstr_eval (_, _) } :: _rest)) -> + raise (Invalid_argument "react.component accepts a record config with props as an options.") + | _ -> defaultProps + [@@raises Invalid_argument] + +(* Plucks the label, loc, and type_ from an AST node *) +let pluckLabelDefaultLocType (label, default, _, _, loc, type_) = (label, default, loc, type_) + +(* Lookup the filename from the location information on the AST node and turn it into a valid module identifier *) +let filenameFromLoc (pstr_loc : Location.t) = + let fileName = match pstr_loc.loc_start.pos_fname with "" -> !Location.input_name | fileName -> fileName in + let fileName = try Filename.chop_extension (Filename.basename fileName) with Invalid_argument _ -> fileName in + let fileName = String.capitalize_ascii fileName in + fileName + +(* Build a string representation of a module name with segments separated by $ *) +let makeModuleName fileName nestedModules fnName = + let fullModuleName = + match (fileName, nestedModules, fnName) with + (* TODO: is this even reachable? It seems like the fileName always exists *) + | "", nestedModules, "make" -> nestedModules + | "", nestedModules, fnName -> List.rev (fnName :: nestedModules) + | fileName, nestedModules, "make" -> fileName :: List.rev nestedModules + | fileName, nestedModules, fnName -> fileName :: List.rev (fnName :: nestedModules) + in + let fullModuleName = String.concat "$" fullModuleName in + fullModuleName + +(* + AST node builders + These functions help us build AST nodes that are needed when transforming a [@react.component] into a + constructor and a props external +*) + +(* Build an AST node representing all named args for the `external` definition for a component's props *) +let rec recursivelyMakeNamedArgsForExternal list args = + match list with + | (label, default, loc, interiorType) :: tl -> + recursivelyMakeNamedArgsForExternal tl + (Typ.arrow ~loc label + ( match (label, interiorType, default) with + (* ~foo=1 *) + | label, None, Some _ -> + { ptyp_desc = Ptyp_var (safeTypeFromValue label); ptyp_loc = loc; ptyp_attributes = [] } + (* ~foo: int=1 *) + | _label, Some type_, Some _ -> type_ + (* ~foo: option(int)=? *) + | label, Some { ptyp_desc = Ptyp_constr ({ txt = Lident "option" }, [ type_ ]) }, _ + | label, Some { ptyp_desc = Ptyp_constr ({ txt = Ldot (Lident "*predef*", "option") }, [ type_ ]) }, _ + (* ~foo: int=? - note this isnt valid. but we want to get a type error *) + | label, Some type_, _ + when isOptional label -> + type_ + (* ~foo=? *) + | label, None, _ when isOptional label -> + { ptyp_desc = Ptyp_var (safeTypeFromValue label); ptyp_loc = loc; ptyp_attributes = [] } + (* ~foo *) + | label, None, _ -> { ptyp_desc = Ptyp_var (safeTypeFromValue label); ptyp_loc = loc; ptyp_attributes = [] } + | _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 loc namedArgListWithKeyAndRef propsType = + let propsName = fnName ^ "Props" in + { + pval_name = { txt = propsName; loc }; + pval_type = + recursivelyMakeNamedArgsForExternal namedArgListWithKeyAndRef + (Typ.arrow nolabel + { ptyp_desc = Ptyp_constr ({ txt = Lident "unit"; loc }, []); ptyp_loc = loc; ptyp_attributes = [] } + propsType); + pval_prim = [ "" ]; + pval_attributes = [ ({ txt = "bs.obj"; loc }, PStr []) ]; + pval_loc = loc; + } + [@@raises Invalid_argument] + +(* Build an AST node representing an `external` with the definition of the [@bs.obj] *) +let makePropsExternal fnName loc namedArgListWithKeyAndRef propsType = + { pstr_loc = loc; pstr_desc = Pstr_primitive (makePropsValue fnName loc namedArgListWithKeyAndRef propsType) } + [@@raises Invalid_argument] + +(* Build an AST node for the signature of the `external` definition *) +let makePropsExternalSig fnName loc namedArgListWithKeyAndRef propsType = + { psig_loc = loc; psig_desc = Psig_value (makePropsValue fnName loc namedArgListWithKeyAndRef propsType) } + [@@raises Invalid_argument] + +(* Build an AST node for the props name when converted to a Js.t inside the function signature *) +let makePropsName ~loc name = { ppat_desc = Ppat_var { txt = name; loc }; ppat_loc = loc; ppat_attributes = [] } + +let makeObjectField loc (str, attrs, type_) = Otag ({ loc; txt = str }, attrs, type_) + +(* Build an AST node representing a "closed" Js.t object representing a component's props *) +let makePropsType ~loc namedTypeList = + Typ.mk ~loc + (Ptyp_constr + ( { txt = Ldot (Lident "Js", "t"); loc }, + [ + { + ptyp_desc = Ptyp_object (List.map (makeObjectField loc) namedTypeList, Closed); + ptyp_loc = loc; + ptyp_attributes = []; + }; + ] )) + +(* Builds an AST node for the entire `external` definition of props *) +let makeExternalDecl fnName loc namedArgListWithKeyAndRef namedTypeList = + makePropsExternal fnName loc + (List.map pluckLabelDefaultLocType namedArgListWithKeyAndRef) + (makePropsType ~loc namedTypeList) + [@@raises Invalid_argument] + +(* TODO: some line number might still be wrong *) +let jsxMapper () = + let jsxVersion = ref None in + + let transformUppercaseCall3 modulePath mapper loc attrs _ callArguments = + let children, argsWithLabels = extractChildren ~loc ~removeLastPositionUnit:true callArguments in + let argsForMake = argsWithLabels in + let childrenExpr = transformChildrenIfListUpper ~loc ~mapper children in + let recursivelyTransformedArgsForMake = + argsForMake |> List.map (fun (label, expression) -> (label, mapper.expr mapper expression)) + in + let childrenArg = ref None in + let args = + recursivelyTransformedArgsForMake + @ ( match childrenExpr with + | Exact children -> [ (labelled "children", children) ] + | ListLiteral { pexp_desc = Pexp_array list } when list = [] -> [] + | ListLiteral expression -> + (* this is a hack to support react components that introspect into their children *) + childrenArg := Some expression; + [ (labelled "children", Exp.ident ~loc { loc; txt = Ldot (Lident "React", "null") }) ] ) + @ [ (nolabel, Exp.construct ~loc { loc; txt = Lident "()" } None) ] + 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] + in + let ident = + match modulePath with + | Lident _ -> Ldot (modulePath, "make") + | Ldot (_modulePath, value) as fullPath when isCap value -> Ldot (fullPath, "make") + | modulePath -> modulePath + in + let propsIdent = + match ident with + | Lident path -> Lident (path ^ "Props") + | Ldot (ident, path) -> Ldot (ident, path ^ "Props") + | _ -> raise (Invalid_argument "JSX name can't be the result of function applications") + in + let props = Exp.apply ~attrs ~loc (Exp.ident ~loc { loc; txt = propsIdent }) args in + (* handle key, ref, children *) + (* React.createElement(Component.make, props, ...children) *) + match !childrenArg with + | None -> + Exp.apply ~loc ~attrs + (Exp.ident ~loc { loc; txt = Ldot (Lident "React", "createElement") }) + [ (nolabel, Exp.ident ~loc { txt = ident; loc }); (nolabel, props) ] + | Some children -> + Exp.apply ~loc ~attrs + (Exp.ident ~loc { loc; txt = Ldot (Lident "React", "createElementVariadic") }) + [ (nolabel, Exp.ident ~loc { txt = ident; loc }); (nolabel, props); (nolabel, children) ] + [@@raises Invalid_argument] + in + + let transformLowercaseCall3 mapper loc attrs callArguments id = + let children, nonChildrenProps = extractChildren ~loc callArguments in + let componentNameExpr = constantString ~loc id in + let childrenExpr = transformChildrenIfList ~loc ~mapper children in + let createElementCall = + match children with + (* [@JSX] div(~children=[a]), coming from
a
*) + | { + pexp_desc = + ( Pexp_construct ({ txt = Lident "::" }, Some { pexp_desc = Pexp_tuple _ }) + | Pexp_construct ({ txt = Lident "[]" }, None) ); + } -> + "createDOMElementVariadic" + (* [@JSX] div(~children= value), coming from
...(value)
*) + | _ -> + raise + (Invalid_argument + "A spread as a DOM element's children don't make sense written together. You can simply remove the \ + spread.") + in + let args = + match nonChildrenProps with + | [ _justTheUnitArgumentAtEnd ] -> + [ (* "div" *) (nolabel, componentNameExpr); (* [|moreCreateElementCallsHere|] *) (nolabel, childrenExpr) ] + | nonEmptyProps -> + let propsCall = + Exp.apply ~loc + (Exp.ident ~loc { loc; txt = Ldot (Lident "ReactDOMRe", "domProps") }) + (nonEmptyProps |> List.map (fun (label, expression) -> (label, mapper.expr mapper expression))) + in + [ + (* "div" *) + (nolabel, componentNameExpr); + (* ReactDOMRe.props(~className=blabla, ~foo=bar, ()) *) + (labelled "props", propsCall); + (* [|moreCreateElementCallsHere|] *) + (nolabel, childrenExpr); + ] + in + Exp.apply ~loc (* throw away the [@JSX] attribute and keep the others, if any *) ~attrs + (* ReactDOMRe.createElement *) + (Exp.ident ~loc { loc; txt = Ldot (Lident "ReactDOMRe", createElementCall) }) + args + [@@raises Invalid_argument] + in + + let rec recursivelyTransformNamedArgsForMake mapper expr list = + let expr = mapper.expr mapper 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.") + | Pexp_fun (arg, default, pattern, expression) when isOptional arg || isLabelled arg -> + let () = + match (isOptional arg, pattern, default) with + | true, { ppat_desc = Ppat_constraint (_, { ptyp_desc }) }, None -> ( + match ptyp_desc with + | Ptyp_constr ({ txt = Lident "option" }, [ _ ]) -> () + | _ -> + let currentType = + match ptyp_desc with + | Ptyp_constr ({ txt }, []) -> String.concat "." (Longident.flatten txt) + | Ptyp_constr ({ txt }, _innerTypeArgs) -> String.concat "." (Longident.flatten txt) ^ "(...)" + | _ -> "..." + in + Location.prerr_warning pattern.ppat_loc + (Preprocessor + (Printf.sprintf + "ReasonReact: optional argument annotations must have explicit `option`. Did you mean \ + `option(%s)=?`?" + currentType)) ) + | _ -> () + in + let alias = + match pattern with + | { ppat_desc = Ppat_alias (_, { txt }) | Ppat_var { txt } } -> txt + | { ppat_desc = Ppat_any } -> "_" + | _ -> getLabel arg + in + let type_ = match pattern with { ppat_desc = Ppat_constraint (_, type_) } -> Some type_ | _ -> None in + + recursivelyTransformNamedArgsForMake mapper expression + ((arg, default, pattern, alias, pattern.ppat_loc, type_) :: list) + | Pexp_fun (Nolabel, _, { ppat_desc = Ppat_construct ({ txt = Lident "()" }, _) | Ppat_any }, _expression) -> + (list, None) + | Pexp_fun + ( Nolabel, + _, + { ppat_desc = Ppat_var { txt } | Ppat_constraint ({ ppat_desc = Ppat_var { txt } }, _) }, + _expression ) -> + (list, Some txt) + | Pexp_fun (Nolabel, _, pattern, _expression) -> + Location.raise_errorf ~loc:pattern.ppat_loc + "ReasonReact: 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_) = + match (type_, name, default) with + | Some { ptyp_desc = Ptyp_constr ({ txt = Lident "option" }, [ type_ ]) }, name, _ when isOptional name -> + ( getLabel name, + [], + { type_ with ptyp_desc = Ptyp_constr ({ loc = type_.ptyp_loc; txt = optionIdent }, [ type_ ]) } ) + :: types + | Some type_, name, Some _default -> + ( getLabel name, + [], + { ptyp_desc = Ptyp_constr ({ loc; txt = optionIdent }, [ type_ ]); ptyp_loc = loc; ptyp_attributes = [] } ) + :: types + | Some type_, name, _ -> (getLabel name, [], type_) :: types + | None, name, _ when isOptional name -> + ( getLabel name, + [], + { + ptyp_desc = + Ptyp_constr + ( { loc; txt = optionIdent }, + [ { ptyp_desc = Ptyp_var (safeTypeFromValue name); ptyp_loc = loc; ptyp_attributes = [] } ] ); + ptyp_loc = loc; + ptyp_attributes = []; + } ) + :: types + | None, name, _ when isLabelled name -> + (getLabel name, [], { ptyp_desc = Ptyp_var (safeTypeFromValue name); ptyp_loc = loc; ptyp_attributes = [] }) + :: types + | _ -> types + [@@raises Invalid_argument] + in + + let argToConcreteType types (name, loc, type_) = + match name with + | name when isLabelled name -> (getLabel name, [], type_) :: types + | name when isOptional name -> (getLabel name, [], Typ.constr ~loc { loc; txt = optionIdent } [ type_ ]) :: types + | _ -> types + in + + let nestedModules = ref [] in + let transformComponentDefinition mapper structure returnStructures = + match structure with + (* external *) + | { + pstr_loc; + pstr_desc = Pstr_primitive ({ pval_name = { txt = fnName }; pval_attributes; pval_type } as value_description); + } as pstr -> ( + match List.filter hasAttr pval_attributes with + | [] -> structure :: returnStructures + | [ _ ] -> + let rec getPropTypes types ({ ptyp_loc; ptyp_desc } as fullType) = + match ptyp_desc with + | Ptyp_arrow (name, type_, ({ ptyp_desc = Ptyp_arrow _ } as rest)) when isLabelled name || isOptional name + -> + getPropTypes ((name, ptyp_loc, type_) :: types) rest + | Ptyp_arrow (Nolabel, _type, rest) -> getPropTypes types rest + | Ptyp_arrow (name, type_, returnValue) when isLabelled name || isOptional name -> + (returnValue, (name, returnValue.ptyp_loc, type_) :: types) + | _ -> (fullType, types) + in + let innerType, propTypes = getPropTypes [] pval_type in + let namedTypeList = List.fold_left argToConcreteType [] propTypes in + let pluckLabelAndLoc (label, loc, type_) = (label, None (* default *), loc, Some type_) in + let retPropsType = makePropsType ~loc:pstr_loc namedTypeList in + let externalPropsDecl = + makePropsExternal fnName pstr_loc + ((optional "key", None, pstr_loc, Some (keyType pstr_loc)) :: List.map pluckLabelAndLoc propTypes) + retPropsType + in + (* can't be an arrow because it will defensively uncurry *) + let newExternalType = + Ptyp_constr ({ loc = pstr_loc; txt = Ldot (Lident "React", "componentLike") }, [ retPropsType; innerType ]) + in + let newStructure = + { + pstr with + pstr_desc = + Pstr_primitive + { + value_description with + pval_type = { pval_type with ptyp_desc = newExternalType }; + pval_attributes = List.filter otherAttrsPure pval_attributes; + }; + } + in + externalPropsDecl :: newStructure :: returnStructures + | _ -> raise (Invalid_argument "Only one react.component call can exist on a component at one time") ) + (* let component = ... *) + | { pstr_loc; pstr_desc = Pstr_value (recFlag, valueBindings) } -> + let fileName = filenameFromLoc pstr_loc in + let emptyLoc = Location.in_file fileName in + let mapBinding binding = + if hasAttrOnBinding binding then + let bindingLoc = binding.pvb_loc in + let bindingPatLoc = binding.pvb_pat.ppat_loc in + let binding = { binding with pvb_pat = { binding.pvb_pat with ppat_loc = emptyLoc }; pvb_loc = emptyLoc } in + let fnName = getFnName binding in + let internalFnName = fnName ^ "$Internal" in + let fullModuleName = makeModuleName fileName !nestedModules fnName in + let modifiedBindingOld binding = + let expression = binding.pvb_expr in + (* TODO: there is a long-tail of unsupported features inside of blocks - Pexp_letmodule , Pexp_letexception , Pexp_ifthenelse *) + let rec spelunkForFunExpression expression = + match expression with + (* let make = (~prop) => ... *) + | { pexp_desc = Pexp_fun _ } -> expression + (* let make = {let foo = bar in (~prop) => ...} *) + | { pexp_desc = Pexp_let (_recursive, _vbs, returnExpression) } -> + (* here's where we spelunk! *) + spelunkForFunExpression returnExpression + (* let make = React.forwardRef((~prop) => ...) *) + | { pexp_desc = Pexp_apply (_wrapperExpression, [ (Nolabel, innerFunctionExpression) ]) } -> + spelunkForFunExpression innerFunctionExpression + | { pexp_desc = Pexp_sequence (_wrapperExpression, innerFunctionExpression) } -> + spelunkForFunExpression innerFunctionExpression + | _ -> + raise + (Invalid_argument + "react.component calls can only be on function definitions or component wrappers (forwardRef, \ + memo).") + [@@raises Invalid_argument] + in + spelunkForFunExpression expression + in + let modifiedBinding binding = + let hasApplication = ref false in + let wrapExpressionWithBinding expressionFn expression = + Vb.mk ~loc:bindingLoc + ~attrs:(List.filter otherAttrsPure binding.pvb_attributes) + (Pat.var ~loc:bindingPatLoc { loc = bindingPatLoc; txt = fnName }) + (expressionFn expression) + in + let expression = binding.pvb_expr in + let unerasableIgnoreExp exp = + { exp with pexp_attributes = unerasableIgnore emptyLoc :: exp.pexp_attributes } + in + (* TODO: there is a long-tail of unsupported features inside of blocks - Pexp_letmodule , Pexp_letexception , Pexp_ifthenelse *) + let rec spelunkForFunExpression expression = + match expression with + (* let make = (~prop) => ... with no final unit *) + | { + pexp_desc = + Pexp_fun + ( ((Labelled _ | Optional _) as label), + default, + pattern, + ({ pexp_desc = Pexp_fun _ } as internalExpression) ); + } -> + let wrap, hasUnit, exp = spelunkForFunExpression internalExpression in + ( wrap, + hasUnit, + unerasableIgnoreExp { expression with pexp_desc = Pexp_fun (label, default, pattern, exp) } ) + (* let make = (()) => ... *) + (* let make = (_) => ... *) + | { + pexp_desc = + Pexp_fun + ( Nolabel, + _default, + { ppat_desc = Ppat_construct ({ txt = Lident "()" }, _) | Ppat_any }, + _internalExpression ); + } -> + ((fun a -> a), true, expression) + (* let make = (~prop) => ... *) + | { pexp_desc = Pexp_fun ((Labelled _ | Optional _), _default, _pattern, _internalExpression) } -> + ((fun a -> a), false, unerasableIgnoreExp expression) + (* let make = (prop) => ... *) + | { pexp_desc = Pexp_fun (_nolabel, _default, pattern, _internalExpression) } -> + if hasApplication.contents then ((fun a -> a), false, unerasableIgnoreExp expression) + else + Location.raise_errorf ~loc:pattern.ppat_loc + "ReasonReact: props need to be labelled arguments.\n\ + \ If you are working with refs be sure to wrap with React.forwardRef.\n\ + \ If your component doesn't have any props use () or _ instead of a name." + (* let make = {let foo = bar in (~prop) => ...} *) + | { pexp_desc = Pexp_let (recursive, vbs, internalExpression) } -> + (* here's where we spelunk! *) + let wrap, hasUnit, exp = spelunkForFunExpression internalExpression in + (wrap, hasUnit, { expression with pexp_desc = Pexp_let (recursive, vbs, exp) }) + (* let make = React.forwardRef((~prop) => ...) *) + | { pexp_desc = Pexp_apply (wrapperExpression, [ (Nolabel, internalExpression) ]) } -> + let () = hasApplication := true in + let _, hasUnit, exp = spelunkForFunExpression internalExpression in + ((fun exp -> Exp.apply wrapperExpression [ (nolabel, exp) ]), hasUnit, exp) + | { pexp_desc = Pexp_sequence (wrapperExpression, internalExpression) } -> + let wrap, hasUnit, exp = spelunkForFunExpression internalExpression in + (wrap, hasUnit, { expression with pexp_desc = Pexp_sequence (wrapperExpression, exp) }) + | e -> ((fun a -> a), false, e) + in + let wrapExpression, hasUnit, expression = spelunkForFunExpression expression in + (wrapExpressionWithBinding wrapExpression, hasUnit, expression) + in + let bindingWrapper, hasUnit, expression = modifiedBinding binding in + let reactComponentAttribute = + try Some (List.find hasAttr binding.pvb_attributes) with Not_found -> None + in + let _attr_loc, payload = + match reactComponentAttribute with + | Some (loc, payload) -> (loc.loc, Some payload) + | None -> (emptyLoc, None) + in + let props = getPropsAttr payload in + (* do stuff here! *) + let namedArgList, forwardRef = + recursivelyTransformNamedArgsForMake mapper (modifiedBindingOld binding) [] + in + let namedArgListWithKeyAndRef = + (optional "key", None, Pat.var { txt = "key"; loc = emptyLoc }, "key", emptyLoc, Some (keyType emptyLoc)) + :: namedArgList + in + let namedArgListWithKeyAndRef = + match forwardRef with + | Some _ -> + (optional "ref", None, Pat.var { txt = "key"; loc = emptyLoc }, "ref", emptyLoc, None) + :: namedArgListWithKeyAndRef + | None -> namedArgListWithKeyAndRef + in + let namedArgListWithKeyAndRefForNew = + match forwardRef with + | Some txt -> namedArgList @ [ (nolabel, None, Pat.var { txt; loc = emptyLoc }, txt, emptyLoc, None) ] + | None -> namedArgList + in + let pluckArg (label, _, _, alias, loc, _) = + let labelString = + match label with label when isOptional label || isLabelled label -> getLabel label | _ -> "" + in + ( label, + match labelString with + | "" -> Exp.ident ~loc { txt = Lident alias; loc } + | labelString -> + Exp.apply ~loc + (Exp.ident ~loc { txt = Lident "##"; loc }) + [ + (nolabel, Exp.ident ~loc { txt = Lident props.propsName; loc }); + (nolabel, Exp.ident ~loc { txt = Lident labelString; loc }); + ] ) + in + let namedTypeList = List.fold_left argToType [] namedArgList in + let loc = emptyLoc in + let externalDecl = makeExternalDecl fnName loc namedArgListWithKeyAndRef namedTypeList in + let innerExpressionArgs = + List.map pluckArg namedArgListWithKeyAndRefForNew + @ if hasUnit then [ (Nolabel, Exp.construct { loc; txt = Lident "()" } None) ] else [] + in + let innerExpression = + Exp.apply + (Exp.ident + { loc; txt = Lident (match recFlag with Recursive -> internalFnName | Nonrecursive -> fnName) }) + innerExpressionArgs + in + let innerExpressionWithRef = + match forwardRef with + | Some txt -> + { + innerExpression with + pexp_desc = + Pexp_fun + ( nolabel, + None, + { ppat_desc = Ppat_var { txt; loc = emptyLoc }; ppat_loc = emptyLoc; ppat_attributes = [] }, + innerExpression ); + } + | None -> innerExpression + in + let fullExpression = + Exp.fun_ nolabel None + { + ppat_desc = + Ppat_constraint + (makePropsName ~loc:emptyLoc props.propsName, makePropsType ~loc:emptyLoc namedTypeList); + ppat_loc = emptyLoc; + ppat_attributes = []; + } + innerExpressionWithRef + in + let fullExpression = + match fullModuleName with + | "" -> fullExpression + | txt -> + Exp.let_ Nonrecursive + [ Vb.mk ~loc:emptyLoc (Pat.var ~loc:emptyLoc { loc = emptyLoc; txt }) fullExpression ] + (Exp.ident ~loc:emptyLoc { loc = emptyLoc; txt = Lident txt }) + in + let bindings, newBinding = + match recFlag with + | Recursive -> + ( [ + bindingWrapper + (Exp.let_ ~loc:emptyLoc Recursive + [ + makeNewBinding binding expression internalFnName; + Vb.mk (Pat.var { loc = emptyLoc; txt = fnName }) fullExpression; + ] + (Exp.ident { loc = emptyLoc; txt = Lident fnName })); + ], + None ) + | Nonrecursive -> + ([ { binding with pvb_expr = expression; pvb_attributes = [] } ], Some (bindingWrapper fullExpression)) + in + (Some externalDecl, bindings, newBinding) + else (None, [ binding ], None) + [@@raises Invalid_argument] + in + let structuresAndBinding = List.map mapBinding valueBindings in + let otherStructures (extern, binding, newBinding) (externs, bindings, newBindings) = + let externs = match extern with Some extern -> extern :: externs | None -> externs in + let newBindings = + match newBinding with Some newBinding -> newBinding :: newBindings | None -> newBindings + in + (externs, binding @ bindings, newBindings) + in + let externs, bindings, newBindings = List.fold_right otherStructures structuresAndBinding ([], [], []) in + externs + @ [ { pstr_loc; pstr_desc = Pstr_value (recFlag, bindings) } ] + @ ( match newBindings with + | [] -> [] + | newBindings -> [ { pstr_loc = emptyLoc; pstr_desc = Pstr_value (recFlag, newBindings) } ] ) + @ returnStructures + | structure -> structure :: returnStructures + [@@raises Invalid_argument] + in + + let reactComponentTransform mapper structures = + List.fold_right (transformComponentDefinition mapper) structures [] + [@@raises Invalid_argument] + in + + let transformComponentSignature _mapper signature returnSignatures = + match signature with + | { psig_loc; psig_desc = Psig_value ({ pval_name = { txt = fnName }; pval_attributes; pval_type } as psig_desc) } + as psig -> ( + match List.filter hasAttr pval_attributes with + | [] -> signature :: returnSignatures + | [ _ ] -> + let rec getPropTypes types ({ ptyp_loc; ptyp_desc } as fullType) = + match ptyp_desc with + | Ptyp_arrow (name, type_, ({ ptyp_desc = Ptyp_arrow _ } as rest)) when isOptional name || isLabelled name + -> + getPropTypes ((name, ptyp_loc, type_) :: types) rest + | Ptyp_arrow (Nolabel, _type, rest) -> getPropTypes types rest + | Ptyp_arrow (name, type_, returnValue) when isOptional name || isLabelled name -> + (returnValue, (name, returnValue.ptyp_loc, type_) :: types) + | _ -> (fullType, types) + in + let innerType, propTypes = getPropTypes [] pval_type in + let namedTypeList = List.fold_left argToConcreteType [] propTypes in + let pluckLabelAndLoc (label, loc, type_) = (label, None, loc, Some type_) in + let retPropsType = makePropsType ~loc:psig_loc namedTypeList in + let externalPropsDecl = + makePropsExternalSig fnName psig_loc + ((optional "key", None, psig_loc, Some (keyType psig_loc)) :: List.map pluckLabelAndLoc propTypes) + retPropsType + in + (* can't be an arrow because it will defensively uncurry *) + let newExternalType = + Ptyp_constr ({ loc = psig_loc; txt = Ldot (Lident "React", "componentLike") }, [ retPropsType; innerType ]) + in + let newStructure = + { + psig with + psig_desc = + Psig_value + { + psig_desc with + pval_type = { pval_type with ptyp_desc = newExternalType }; + pval_attributes = List.filter otherAttrsPure pval_attributes; + }; + } + in + externalPropsDecl :: newStructure :: returnSignatures + | _ -> raise (Invalid_argument "Only one react.component call can exist on a component at one time") ) + | signature -> signature :: returnSignatures + [@@raises Invalid_argument] + in + + let reactComponentSignatureTransform mapper signatures = + List.fold_right (transformComponentSignature mapper) signatures [] + [@@raises Invalid_argument] + in + + let transformJsxCall mapper callExpression callArguments attrs = + match callExpression.pexp_desc with + | Pexp_ident caller -> ( + match caller with + | { txt = Lident "createElement" } -> + raise (Invalid_argument "JSX: `createElement` should be preceeded by a module name.") + (* Foo.createElement(~prop1=foo, ~prop2=bar, ~children=[], ()) *) + | { loc; txt = Ldot (modulePath, ("createElement" | "make")) } -> ( + match !jsxVersion with + | None | Some 3 -> transformUppercaseCall3 modulePath mapper loc attrs callExpression callArguments + | Some _ -> raise (Invalid_argument "JSX: the JSX version must be 3") ) + (* div(~prop1=foo, ~prop2=bar, ~children=[bla], ()) *) + (* turn that into + ReactDOMRe.createElement(~props=ReactDOMRe.props(~props1=foo, ~props2=bar, ()), [|bla|]) *) + | { loc; txt = Lident id } -> ( + match !jsxVersion with + | None | Some 3 -> transformLowercaseCall3 mapper loc attrs callArguments id + | Some _ -> raise (Invalid_argument "JSX: the JSX version must be 3") ) + | { txt = Ldot (_, anythingNotCreateElementOrMake) } -> + raise + (Invalid_argument + ( "JSX: the JSX attribute should be attached to a `YourModuleName.createElement` or \ + `YourModuleName.make` call. We saw `" ^ anythingNotCreateElementOrMake ^ "` instead" )) + | { txt = Lapply _ } -> + (* don't think there's ever a case where this is reached *) + raise (Invalid_argument "JSX: encountered a weird case while processing the code. Please report this!") ) + | _ -> raise (Invalid_argument "JSX: `createElement` should be preceeded by a simple, direct module name.") + [@@raises Invalid_argument] + in + + let signature mapper signature = + default_mapper.signature mapper @@ reactComponentSignatureTransform mapper signature + [@@raises Invalid_argument] + in + + let structure mapper structure = + match structure with structures -> default_mapper.structure mapper @@ reactComponentTransform mapper structures + [@@raises Invalid_argument] + in + + let expr mapper expression = + match expression with + (* Does the function application have the @JSX attribute? *) + | { pexp_desc = Pexp_apply (callExpression, callArguments); pexp_attributes } -> ( + let jsxAttribute, nonJSXAttributes = + List.partition (fun (attribute, _) -> attribute.txt = "JSX") pexp_attributes + in + match (jsxAttribute, nonJSXAttributes) with + (* no JSX attribute *) + | [], _ -> default_mapper.expr mapper expression + | _, nonJSXAttributes -> transformJsxCall mapper callExpression callArguments nonJSXAttributes ) + (* is it a list with jsx attribute? Reason <>foo desugars to [@JSX][foo]*) + | { + pexp_desc = + ( Pexp_construct ({ txt = Lident "::"; loc }, Some { pexp_desc = Pexp_tuple _ }) + | Pexp_construct ({ txt = Lident "[]"; loc }, None) ); + pexp_attributes; + } as listItems -> ( + let jsxAttribute, nonJSXAttributes = + List.partition (fun (attribute, _) -> attribute.txt = "JSX") pexp_attributes + in + match (jsxAttribute, nonJSXAttributes) with + (* no JSX attribute *) + | [], _ -> default_mapper.expr mapper expression + | _, nonJSXAttributes -> + let fragment = Exp.ident ~loc { loc; txt = Ldot (Lident "ReasonReact", "fragment") } in + let childrenExpr = transformChildrenIfList ~loc ~mapper listItems in + let args = + [ (* "div" *) (nolabel, fragment); (* [|moreCreateElementCallsHere|] *) (nolabel, childrenExpr) ] + in + Exp.apply ~loc (* throw away the [@JSX] attribute and keep the others, if any *) ~attrs:nonJSXAttributes + (* ReactDOMRe.createElement *) + (Exp.ident ~loc { loc; txt = Ldot (Lident "ReactDOMRe", "createElement") }) + args ) + (* Delegate to the default mapper, a deep identity traversal *) + | e -> default_mapper.expr mapper e + [@@raises Invalid_argument] + in + + let module_binding mapper module_binding = + let _ = nestedModules := module_binding.pmb_name.txt :: !nestedModules in + let mapped = default_mapper.module_binding mapper module_binding in + let _ = nestedModules := List.tl !nestedModules in + mapped + [@@raises Failure] + in + { default_mapper with structure; expr; signature; module_binding } + [@@raises Invalid_argument, Failure] + +let rewrite_implementation (code : Parsetree.structure) : Parsetree.structure = + let mapper = jsxMapper () in + mapper.structure mapper code + [@@raises Invalid_argument, Failure] + +let rewrite_signature (code : Parsetree.signature) : Parsetree.signature = + let mapper = jsxMapper () in + mapper.signature mapper code + [@@raises Invalid_argument, Failure] diff --git a/src/reactjs_jsx_ppx.mli b/src/reactjs_jsx_ppx.mli new file mode 100644 index 00000000..da60a051 --- /dev/null +++ b/src/reactjs_jsx_ppx.mli @@ -0,0 +1,39 @@ +(* + This is the module that handles turning Reason JSX' agnostic function call into + a ReasonReact-specific function call. Aka, this is a macro, using OCaml's ppx + facilities; https://whitequark.org/blog/2014/04/16/a-guide-to-extension- + points-in-ocaml/ + You wouldn't use this file directly; it's used by ReScript's + bsconfig.json. Specifically, there's a field called `react-jsx` inside the + field `reason`, which enables this ppx through some internal call in bsb +*) + +(* + There are two different transforms that can be selected in this file (v2 and v3): + v2: + transform `[@JSX] div(~props1=a, ~props2=b, ~children=[foo, bar], ())` into + `ReactDOMRe.createElement("div", ~props={"props1": 1, "props2": b}, [|foo, + bar|])`. + transform `[@JSX] div(~props1=a, ~props2=b, ~children=foo, ())` into + `ReactDOMRe.createElementVariadic("div", ~props={"props1": 1, "props2": b}, foo)`. + transform the upper-cased case + `[@JSX] Foo.createElement(~key=a, ~ref=b, ~foo=bar, ~children=[], ())` into + `ReasonReact.element(~key=a, ~ref=b, Foo.make(~foo=bar, [||]))` + transform `[@JSX] [foo]` into + `ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])` + v3: + transform `[@JSX] div(~props1=a, ~props2=b, ~children=[foo, bar], ())` into + `ReactDOMRe.createDOMElementVariadic("div", ReactDOMRe.domProps(~props1=1, ~props2=b), [|foo, bar|])`. + transform the upper-cased case + `[@JSX] Foo.createElement(~key=a, ~ref=b, ~foo=bar, ~children=[], ())` into + `React.createElement(Foo.make, Foo.makeProps(~key=a, ~ref=b, ~foo=bar, ()))` + transform the upper-cased case + `[@JSX] Foo.createElement(~foo=bar, ~children=[foo, bar], ())` into + `React.createElementVariadic(Foo.make, Foo.makeProps(~foo=bar, ~children=React.null, ()), [|foo, bar|])` + transform `[@JSX] [foo]` into + `ReactDOMRe.createElement(ReasonReact.fragment, [|foo|])` +*) + +val rewrite_implementation : Parsetree.structure -> Parsetree.structure + +val rewrite_signature : Parsetree.signature -> Parsetree.signature diff --git a/src/res_cli.ml b/src/res_cli.ml index 9159b6da..e750c3b7 100644 --- a/src/res_cli.ml +++ b/src/res_cli.ml @@ -207,11 +207,12 @@ module CliArgProcessor = struct let processInterface = isInterface || len > 0 && (String.get [@doesNotRaise]) filename (len - 1) = 'i' in - let parsingEngine = + let parsingEngine, react = match origin with - | "reasonBinary" -> Parser Res_driver_reason_binary.parsingEngine - | "ml" | "ocaml" -> Parser Res_driver_ml_parser.parsingEngine - | _ -> Parser Res_driver.parsingEngine + | "reasonBinary" -> Parser Res_driver_reason_binary.parsingEngine, false + | "ml" | "ocaml" -> Parser Res_driver_ml_parser.parsingEngine, false + | "reactJsx" -> Parser Res_driver.parsingEngine, true + | _ -> Parser Res_driver.parsingEngine, false in let printEngine = match target with @@ -237,14 +238,19 @@ module CliArgProcessor = struct ~source:parseResult.source ~filename:parseResult.filename parseResult.diagnostics; - if recover || not parseResult.invalid then + if react then + exit 1 + else if recover then printEngine.printInterface ~width ~filename ~comments:parseResult.comments parseResult.parsetree else () end else + let parsetree = + if react then Reactjs_jsx_ppx.rewrite_signature parseResult.parsetree else parseResult.parsetree + in printEngine.printInterface - ~width ~filename ~comments:parseResult.comments parseResult.parsetree + ~width ~filename ~comments:parseResult.comments parsetree else let parseResult = backend.parseImplementation ~forPrinter ~filename in if parseResult.invalid then begin @@ -252,25 +258,30 @@ module CliArgProcessor = struct ~source:parseResult.source ~filename:parseResult.filename parseResult.diagnostics; - if recover || not parseResult.invalid then + if react then + exit 1 + else if recover then printEngine.printImplementation ~width ~filename ~comments:parseResult.comments parseResult.parsetree else () end else + let parsetree = + if react then Reactjs_jsx_ppx.rewrite_implementation parseResult.parsetree else parseResult.parsetree + in printEngine.printImplementation - ~width ~filename ~comments:parseResult.comments parseResult.parsetree + ~width ~filename ~comments:parseResult.comments parsetree with | Failure txt -> prerr_string txt; prerr_newline(); exit 1 | _ -> exit 1 - [@@raises exit] + [@@raises Invalid_argument, exit] end -let [@raises exit] () = +let [@raises Invalid_argument, exit] () = if not !Sys.interactive then begin ResClflags.parse (); match !ResClflags.files with diff --git a/src/res_multi_printer.ml b/src/res_multi_printer.ml index cfcf1942..220f1071 100644 --- a/src/res_multi_printer.ml +++ b/src/res_multi_printer.ml @@ -51,6 +51,40 @@ let printMl ~isInterface ~filename = ~comments:parseResult.comments parseResult.parsetree +(* print files using React JSX ppx to res syntax *) +let printJsx ~isInterface ~filename = + if isInterface then + let parseResult = + Res_driver.parsingEngine.parseInterface ~forPrinter:true ~filename + in + if parseResult.invalid then + begin + Res_diagnostics.printReport parseResult.diagnostics parseResult.source; + exit 1 + end + else + let jsxPass = Reactjs_jsx_ppx.rewrite_signature parseResult.parsetree in + Res_printer.printInterface + ~width:defaultPrintWidth + ~comments:parseResult.comments + jsxPass + else + let parseResult = + Res_driver.parsingEngine.parseImplementation ~forPrinter:true ~filename + in + if parseResult.invalid then + begin + Res_diagnostics.printReport parseResult.diagnostics parseResult.source; + exit 1 + end + else + let jsxPass = Reactjs_jsx_ppx.rewrite_implementation parseResult.parsetree in + Res_printer.printImplementation + ~width:defaultPrintWidth + ~comments:parseResult.comments + jsxPass +[@@raises Invalid_argument, Failure, exit] + (* How does printing Reason to Res work? * -> open a tempfile * -> write the source code found in "filename" into the tempfile @@ -122,7 +156,8 @@ let print language ~input = len > 0 && String.unsafe_get input (len - 1) = 'i' in match language with + | `jsx -> printJsx ~isInterface ~filename:input | `res -> printRes ~isInterface ~filename:input | `ml -> printMl ~isInterface ~filename:input | `refmt path -> printReason ~refmtPath:path ~isInterface ~filename:input -[@@raises Sys_error, exit] +[@@raises Invalid_argument, Failure, Sys_error, exit] diff --git a/src/res_multi_printer.mli b/src/res_multi_printer.mli index 1a1d9624..8e5f8ca2 100644 --- a/src/res_multi_printer.mli +++ b/src/res_multi_printer.mli @@ -1,3 +1,3 @@ (* Interface to print source code from different languages to res. * Takes a filename called "input" and returns the corresponding formatted res syntax *) -val print: [`ml | `res | `refmt of string (* path to refmt *)] -> input: string -> string +val print: [`jsx | `ml | `res | `refmt of string (* path to refmt *)] -> input: string -> string diff --git a/tests/api/resReactJsx.res b/tests/api/resReactJsx.res new file mode 100644 index 00000000..e236abf2 --- /dev/null +++ b/tests/api/resReactJsx.res @@ -0,0 +1,6 @@ +// test React JSX file + +@react.component +let make = (~msg) => { +
{msg->React.string}
+} diff --git a/tests/printer/reactJsx/__snapshots__/render.spec.js.snap b/tests/printer/reactJsx/__snapshots__/render.spec.js.snap new file mode 100644 index 00000000..0d5677a2 --- /dev/null +++ b/tests/printer/reactJsx/__snapshots__/render.spec.js.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`commentAtTop.react.res 1`] = ` +"@bs.obj +external makeProps: ( + ~msg: 'msg, + ~key: string=?, + unit, +) => { + \\"msg\\": 'msg, + // test React JSX file +} = \\"\\" + +let make = + (@warning(\\"-16\\") ~msg) => { + ReactDOMRe.createDOMElementVariadic(\\"div\\", [{msg->React.string}]) + } +let make = { + let \\\\\\"CommentAtTop.react\\" = (\\\\\\"Props\\": {\\"msg\\": 'msg}) => + make(~msg=\\\\\\"Props\\"[\\"msg\\"]) + \\\\\\"CommentAtTop.react\\" +} +" +`; + +exports[`externalWithCustomName.react.res.fixme 1`] = `""`; + +exports[`innerModule.react.res 1`] = ` +"module Bar = { + @bs.obj + external makeProps: ( + ~a: 'a, + ~b: 'b, + ~key: string=?, + unit, + ) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\" + let make = + (@warning(\\"-16\\") ~a, @warning(\\"-16\\") ~b, _) => { + Js.log(\\"This function should be named \`Test$Bar\`\\") + ReactDOMRe.createDOMElementVariadic(\\"div\\", []) + } + let make = { + let \\\\\\"InnerModule.react$Bar\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => + make(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ()) + \\\\\\"InnerModule.react$Bar\\" + } + @bs.obj + external componentProps: ( + ~a: 'a, + ~b: 'b, + ~key: string=?, + unit, + ) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\" + + let component = + (@warning(\\"-16\\") ~a, @warning(\\"-16\\") ~b, _) => { + Js.log(\\"This function should be named \`Test$Bar$component\`\\") + ReactDOMRe.createDOMElementVariadic(\\"div\\", []) + } + let component = { + let \\\\\\"InnerModule.react$Bar$component\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => + component(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ()) + \\\\\\"InnerModule.react$Bar$component\\" + } +} +" +`; + +exports[`topLevel.react.res 1`] = ` +"@bs.obj +external makeProps: ( + ~a: 'a, + ~b: 'b, + ~key: string=?, + unit, +) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\" +let make = + (@warning(\\"-16\\") ~a, @warning(\\"-16\\") ~b, _) => { + Js.log(\\"This function should be named 'TopLevel.react'\\") + ReactDOMRe.createDOMElementVariadic(\\"div\\", []) + } +let make = { + let \\\\\\"TopLevel.react\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => + make(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ()) + \\\\\\"TopLevel.react\\" +} +" +`; diff --git a/tests/printer/reactJsx/commentAtTop.react.res b/tests/printer/reactJsx/commentAtTop.react.res new file mode 100644 index 00000000..e236abf2 --- /dev/null +++ b/tests/printer/reactJsx/commentAtTop.react.res @@ -0,0 +1,6 @@ +// test React JSX file + +@react.component +let make = (~msg) => { +
{msg->React.string}
+} diff --git a/tests/printer/reactJsx/externalWithCustomName.react.res.fixme b/tests/printer/reactJsx/externalWithCustomName.react.res.fixme new file mode 100644 index 00000000..914a40ea --- /dev/null +++ b/tests/printer/reactJsx/externalWithCustomName.react.res.fixme @@ -0,0 +1,6 @@ +module Foo = { + @react.component @bs.module("Foo") + external component: (~a: int, ~b: string, _) => React.element = "component" +} + +let t = diff --git a/tests/printer/reactJsx/innerModule.react.res b/tests/printer/reactJsx/innerModule.react.res new file mode 100644 index 00000000..f010591d --- /dev/null +++ b/tests/printer/reactJsx/innerModule.react.res @@ -0,0 +1,16 @@ +module Bar = { + @react.component + let make = (~a, ~b, _) => { + Js.log( + "This function should be named `Test$Bar`", + ) +
+ } + @react.component + let component = (~a, ~b, _) => { + Js.log( + "This function should be named `Test$Bar$component`", + ) +
+ } +} diff --git a/tests/printer/reactJsx/render.spec.js b/tests/printer/reactJsx/render.spec.js new file mode 100644 index 00000000..3d3252b8 --- /dev/null +++ b/tests/printer/reactJsx/render.spec.js @@ -0,0 +1 @@ +runPrinter(__dirname, false, false) diff --git a/tests/printer/reactJsx/topLevel.react.res b/tests/printer/reactJsx/topLevel.react.res new file mode 100644 index 00000000..3c00888e --- /dev/null +++ b/tests/printer/reactJsx/topLevel.react.res @@ -0,0 +1,5 @@ +@react.component +let make = (~a, ~b, _) => { + Js.log("This function should be named 'TopLevel.react'") +
+} diff --git a/tests/res_test.ml b/tests/res_test.ml index b3ab7dd0..2b62c8e8 100644 --- a/tests/res_test.ml +++ b/tests/res_test.ml @@ -126,6 +126,31 @@ let y: float |} ) +(* test printing of React jsx file *) +let () = +let filename = "./tests/api/resReactJsx.res" in +let prettySource = Res_multi_printer.print `jsx ~input:filename in +assert ( + prettySource = {|@bs.obj +external makeProps: ( + ~msg: 'msg, + ~key: string=?, + unit, +) => { + "msg": 'msg, + // test React JSX file +} = "" + +let make = + (@warning("-16") ~msg) => { + ReactDOMRe.createDOMElementVariadic("div", [{msg->React.string}]) + } +let make = { + let \"ResReactJsx" = (\"Props": {"msg": 'msg}) => make(~msg=\"Props"["msg"]) + \"ResReactJsx" +} +|}) + let () = print_endline "✅ multi printer api tests" module OutcomePrinterTests = struct diff --git a/tests/runner.js b/tests/runner.js index bb9c5d89..b7048807 100644 --- a/tests/runner.js +++ b/tests/runner.js @@ -29,6 +29,12 @@ function classifyLang(filename) { return "ocaml"; } } + if (filename.length > 10) { + let suffix = filename.substring(filename.length - 10); + if (suffix === ".react.res" || suffix === "react.resi") { + return "reactJsx"; + } + } return "rescript"; } @@ -81,10 +87,10 @@ function parseReasonFileToSexp(filename) { .stdout.toString(); } -function parseNapkinFileToSexp(filename) { +function parseNapkinFileToSexp(filename, reactJsx) { let { stdout } = cp.spawnSync(parser, [ "-parse", - "res", + reactJsx ? "reactJsx" : "res", "-print", "sexp", filename, @@ -102,8 +108,11 @@ function parseFileToSexp(filename) { case "ocaml": return parseOcamlFileToSexp(filename); + case "reactJsx": + return parseOcamlFileToSexp(filename, true); + default: - return parseNapkinFileToSexp(filename); + return parseNapkinFileToSexp(filename, false); } } @@ -154,7 +163,15 @@ function printFile(filename) { case "reason": parserSrc = "re"; - return parseReasonFileToNapkin(filename, 80); + return { + result: parseReasonFileToNapkin(filename, 80), + status: 0, + errorOutput: "" + }; + break; + + case "reactJsx": + parserSrc = "reactJsx"; break; case "rescript": @@ -164,7 +181,6 @@ function printFile(filename) { } let intf = isInterface(filename); - let args = ["-parse", parserSrc, "-print", "res", "-width", "80"]; if (intf) { @@ -173,7 +189,13 @@ function printFile(filename) { args.push(filename); - return cp.spawnSync(parser, args).stdout.toString("utf8"); + let result = cp.spawnSync(parser, args); + + return { + result: result.stdout.toString("utf8"), + status: result.status, + errorOutput: result.stderr + } } /* Parser error output format: @@ -196,16 +218,20 @@ global.runPrinter = (dirname) => { } test(base, () => { - let napkin = printFile(filename); - expect(napkin).toMatchSnapshot(); + let {result, errorOutput, status} = printFile(filename); + if (status > 0) { + fail(new Error(`Test from file: ${filename} failed with error output: ${errorOutput}`)); + } else { + expect(result).toMatchSnapshot(); + } if (process.env.ROUNDTRIP_TEST) { let intf = isInterface(filename); let sexpAst = parseFileToSexp(filename); - let napkin2 = parseNapkinStdinToNapkin(napkin, intf, 80); - let napkinSexpAst = parseNapkinStdinToSexp(napkin, intf); - expect(sexpAst).toEqual(napkinSexpAst); - expect(napkin).toEqual(napkin2); + let result2 = parseNapkinStdinToNapkin(result, intf, 80); + let resultSexpAst = parseNapkinStdinToSexp(result, intf); + expect(sexpAst).toEqual(resultSexpAst); + expect(result).toEqual(result2); } }); }); From d24c0dfc7438aa2c69c8d4158200f618a8feb853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cha=CC=81varri?= Date: Sun, 30 Aug 2020 21:43:53 +0200 Subject: [PATCH 2/8] Fix roundtrip tests, ignore test files with .fixme suffix --- .../printer/reactJsx/__snapshots__/render.spec.js.snap | 6 ++---- tests/printer/reactJsx/innerModule.react.res | 4 ++-- tests/runner.js | 10 +++++++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/printer/reactJsx/__snapshots__/render.spec.js.snap b/tests/printer/reactJsx/__snapshots__/render.spec.js.snap index 0d5677a2..37c56338 100644 --- a/tests/printer/reactJsx/__snapshots__/render.spec.js.snap +++ b/tests/printer/reactJsx/__snapshots__/render.spec.js.snap @@ -23,8 +23,6 @@ let make = { " `; -exports[`externalWithCustomName.react.res.fixme 1`] = `""`; - exports[`innerModule.react.res 1`] = ` "module Bar = { @bs.obj @@ -36,7 +34,7 @@ exports[`innerModule.react.res 1`] = ` ) => {\\"a\\": 'a, \\"b\\": 'b} = \\"\\" let make = (@warning(\\"-16\\") ~a, @warning(\\"-16\\") ~b, _) => { - Js.log(\\"This function should be named \`Test$Bar\`\\") + Js.log(\\"This function should be named \`InnerModule.react$Bar\`\\") ReactDOMRe.createDOMElementVariadic(\\"div\\", []) } let make = { @@ -54,7 +52,7 @@ exports[`innerModule.react.res 1`] = ` let component = (@warning(\\"-16\\") ~a, @warning(\\"-16\\") ~b, _) => { - Js.log(\\"This function should be named \`Test$Bar$component\`\\") + Js.log(\\"This function should be named \`InnerModule.react$Bar$component\`\\") ReactDOMRe.createDOMElementVariadic(\\"div\\", []) } let component = { diff --git a/tests/printer/reactJsx/innerModule.react.res b/tests/printer/reactJsx/innerModule.react.res index f010591d..08b7c482 100644 --- a/tests/printer/reactJsx/innerModule.react.res +++ b/tests/printer/reactJsx/innerModule.react.res @@ -2,14 +2,14 @@ module Bar = { @react.component let make = (~a, ~b, _) => { Js.log( - "This function should be named `Test$Bar`", + "This function should be named `InnerModule.react$Bar`", )
} @react.component let component = (~a, ~b, _) => { Js.log( - "This function should be named `Test$Bar$component`", + "This function should be named `InnerModule.react$Bar$component`", )
} diff --git a/tests/runner.js b/tests/runner.js index b7048807..96068140 100644 --- a/tests/runner.js +++ b/tests/runner.js @@ -109,7 +109,7 @@ function parseFileToSexp(filename) { return parseOcamlFileToSexp(filename); case "reactJsx": - return parseOcamlFileToSexp(filename, true); + return parseNapkinFileToSexp(filename, true); default: return parseNapkinFileToSexp(filename, false); @@ -213,6 +213,14 @@ let makeReproducibleFilename = (txt) => { global.runPrinter = (dirname) => { fs.readdirSync(dirname).forEach((base) => { let filename = path.join(dirname, base); + + if (filename.length > 6) { + let suffix = filename.substring(filename.length - 6); + if (suffix === ".fixme") { + return; + } + } + if (!fs.lstatSync(filename).isFile() || base === "render.spec.js") { return; } From 8a7a1bdebc8a3e053f588a3b702c11c9086ffe5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cha=CC=81varri?= Date: Tue, 22 Sep 2020 00:11:39 +0200 Subject: [PATCH 3/8] add -ppx cmd line arg --- src/res_cli.ml | 31 ++++++++++++++++++--------- tests/printer/reactJsx/render.spec.js | 2 +- tests/runner.js | 30 +++++++------------------- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/res_cli.ml b/src/res_cli.ml index 865f6acb..66ab57d2 100644 --- a/src/res_cli.ml +++ b/src/res_cli.ml @@ -165,6 +165,7 @@ module ResClflags: sig val files: string list ref val interface: bool ref val report: string ref + val ppx: string ref val parse: unit -> unit end = struct @@ -178,6 +179,7 @@ end = struct let origin = ref "" let interface = ref false let report = ref "pretty" + let ppx = ref "none" let usage = "Usage:\n rescript \n\n" ^ "Examples:\n" ^ @@ -192,6 +194,7 @@ end = struct ("-print", Arg.String (fun txt -> print := txt), "Print either binary or ns. Default: ns"); ("-width", Arg.Int (fun w -> width := w), "Specify the line length for the printer (formatter)"); ("-interface", Arg.Unit (fun () -> interface := true), "Parse as interface"); + ("-ppx", Arg.String (fun txt -> ppx := txt), "Apply a specific built-in ppx before parsing, none or jsx. Default: none"); (* ("-report", Arg.String (fun txt -> report := txt), "Stylize errors and messages using color and context. Accepts `Pretty` and `Plain`. Default `Plain`") *) ] @@ -201,18 +204,17 @@ end module CliArgProcessor = struct type backend = Parser: ('diagnostics) Res_driver.parsingEngine -> backend [@@unboxed] - let processFile ~isInterface ~width ~recover ~origin ~target ~report:_ filename = + let processFile ~isInterface ~width ~recover ~origin ~target ~report:_ ~ppx filename = try let len = String.length filename in let processInterface = isInterface || len > 0 && (String.get [@doesNotRaise]) filename (len - 1) = 'i' in - let parsingEngine, react = + let parsingEngine = match origin with - | "reasonBinary" -> Parser Res_driver_reason_binary.parsingEngine, false - | "ml" | "ocaml" -> Parser Res_driver_ml_parser.parsingEngine, false - | "reactJsx" -> Parser Res_driver.parsingEngine, true - | _ -> Parser Res_driver.parsingEngine, false + | "reasonBinary" -> Parser Res_driver_reason_binary.parsingEngine + | "ml" | "ocaml" -> Parser Res_driver_ml_parser.parsingEngine + | _ -> Parser Res_driver.parsingEngine in let printEngine = match target with @@ -228,6 +230,11 @@ module CliArgProcessor = struct | _ -> false in + let ppx = match ppx with + | "jsx" -> `Jsx + | _ -> `None + in + let Parser backend = parsingEngine in (* This is the whole purpose of the Color module above *) Color.setup None; @@ -244,8 +251,9 @@ module CliArgProcessor = struct else exit 1 end else - let parsetree = - if react then Reactjs_jsx_ppx.rewrite_signature parseResult.parsetree else parseResult.parsetree + let parsetree = match ppx with + | `Jsx -> Reactjs_jsx_ppx.rewrite_signature parseResult.parsetree + | `None -> parseResult.parsetree in printEngine.printInterface ~width ~filename ~comments:parseResult.comments parsetree @@ -262,8 +270,9 @@ module CliArgProcessor = struct else exit 1 end else - let parsetree = - if react then Reactjs_jsx_ppx.rewrite_implementation parseResult.parsetree else parseResult.parsetree + let parsetree = match ppx with + | `Jsx -> Reactjs_jsx_ppx.rewrite_implementation parseResult.parsetree + | `None -> parseResult.parsetree in printEngine.printImplementation ~width ~filename ~comments:parseResult.comments parsetree @@ -289,6 +298,7 @@ let [@raises Invalid_argument, exit] () = ~target:!ResClflags.print ~origin:!ResClflags.origin ~report:!ResClflags.report + ~ppx:!ResClflags.ppx "" | files -> List.iter (fun filename -> @@ -299,6 +309,7 @@ let [@raises Invalid_argument, exit] () = ~target:!ResClflags.print ~origin:!ResClflags.origin ~report:!ResClflags.report + ~ppx:!ResClflags.ppx filename ) files end diff --git a/tests/printer/reactJsx/render.spec.js b/tests/printer/reactJsx/render.spec.js index 3d3252b8..c5674ab7 100644 --- a/tests/printer/reactJsx/render.spec.js +++ b/tests/printer/reactJsx/render.spec.js @@ -1 +1 @@ -runPrinter(__dirname, false, false) +runPrinter(__dirname, "jsx") diff --git a/tests/runner.js b/tests/runner.js index 55018b1c..cfeff587 100644 --- a/tests/runner.js +++ b/tests/runner.js @@ -29,12 +29,6 @@ function classifyLang(filename) { return "ocaml"; } } - if (filename.length > 10) { - let suffix = filename.substring(filename.length - 10); - if (suffix === ".react.res" || suffix === "react.resi") { - return "reactJsx"; - } - } return "rescript"; } @@ -93,10 +87,10 @@ function parseReasonFileToSexp(filename) { .stdout.toString(); } -function parseNapkinFileToSexp(filename, reactJsx) { +function parseNapkinFileToSexp(filename) { let { stdout } = cp.spawnSync(parser, [ "-parse", - reactJsx ? "reactJsx" : "res", + "res", "-print", "sexp", filename, @@ -114,11 +108,8 @@ function parseFileToSexp(filename) { case "ocaml": return parseOcamlFileToSexp(filename); - case "reactJsx": - return parseNapkinFileToSexp(filename, true); - default: - return parseNapkinFileToSexp(filename, false); + return parseNapkinFileToSexp(filename); } } @@ -160,7 +151,7 @@ function parseNapkinStdinToNapkin(src, isInterface, width = 100) { .stdout.toString("utf8"); } -function printFile(filename) { +function printFile(filename, ppx) { let parserSrc; switch (classifyLang(filename)) { case "ocaml": @@ -174,12 +165,7 @@ function printFile(filename) { status: 0, errorOutput: "" }; - break; - - case "reactJsx": - parserSrc = "reactJsx"; - break; - + case "rescript": default: parserSrc = "res"; @@ -187,7 +173,7 @@ function printFile(filename) { } let intf = isInterface(filename); - let args = ["-parse", parserSrc, "-print", "res", "-width", "80"]; + let args = ["-parse", parserSrc, "-print", "res", "-width", "80", "-ppx", ppx]; if (intf) { args.push("-interface"); @@ -216,7 +202,7 @@ let makeReproducibleFilename = (txt) => { }) }; -global.runPrinter = (dirname) => { +global.runPrinter = (dirname, ppx = "none") => { fs.readdirSync(dirname).forEach((base) => { let filename = path.join(dirname, base); @@ -232,7 +218,7 @@ global.runPrinter = (dirname) => { } test(base, () => { - let {result, errorOutput, status} = printFile(filename); + let {result, errorOutput, status} = printFile(filename, ppx); if (status > 0) { let msg = `Test from file: ${filename} failed with error output: From d3dcd85a1618835db3b0740dc67760f5a8194e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cha=CC=81varri?= Date: Tue, 22 Sep 2020 00:14:23 +0200 Subject: [PATCH 4/8] rename .react.res to just .res --- .../__snapshots__/render.spec.js.snap | 23 +++++++++---------- ...ommentAtTop.react.res => commentAtTop.res} | 0 ...fixme => externalWithCustomName.res.fixme} | 0 ...{innerModule.react.res => innerModule.res} | 0 .../{topLevel.react.res => topLevel.res} | 0 5 files changed, 11 insertions(+), 12 deletions(-) rename tests/printer/reactJsx/{commentAtTop.react.res => commentAtTop.res} (100%) rename tests/printer/reactJsx/{externalWithCustomName.react.res.fixme => externalWithCustomName.res.fixme} (100%) rename tests/printer/reactJsx/{innerModule.react.res => innerModule.res} (100%) rename tests/printer/reactJsx/{topLevel.react.res => topLevel.res} (100%) diff --git a/tests/printer/reactJsx/__snapshots__/render.spec.js.snap b/tests/printer/reactJsx/__snapshots__/render.spec.js.snap index 38af1c3d..ddca8f7e 100644 --- a/tests/printer/reactJsx/__snapshots__/render.spec.js.snap +++ b/tests/printer/reactJsx/__snapshots__/render.spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`commentAtTop.react.res 1`] = ` +exports[`commentAtTop.res 1`] = ` "@bs.obj external makeProps: (~msg: 'msg, ~key: string=?, unit) => {\\"msg\\": 'msg} = \\"\\" // test React JSX file @@ -9,14 +9,13 @@ let make = ReactDOMRe.createDOMElementVariadic(\\"div\\", [{msg->React.string}]) } let make = { - let \\\\\\"CommentAtTop.react\\" = (\\\\\\"Props\\": {\\"msg\\": 'msg}) => - make(~msg=\\\\\\"Props\\"[\\"msg\\"]) - \\\\\\"CommentAtTop.react\\" + let \\\\\\"CommentAtTop\\" = (\\\\\\"Props\\": {\\"msg\\": 'msg}) => make(~msg=\\\\\\"Props\\"[\\"msg\\"]) + \\\\\\"CommentAtTop\\" } " `; -exports[`innerModule.react.res 1`] = ` +exports[`innerModule.res 1`] = ` "module Bar = { @bs.obj external makeProps: ( @@ -31,9 +30,9 @@ exports[`innerModule.react.res 1`] = ` ReactDOMRe.createDOMElementVariadic(\\"div\\", []) } let make = { - let \\\\\\"InnerModule.react$Bar\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => + let \\\\\\"InnerModule$Bar\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => make(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ()) - \\\\\\"InnerModule.react$Bar\\" + \\\\\\"InnerModule$Bar\\" } @bs.obj external componentProps: ( @@ -49,15 +48,15 @@ exports[`innerModule.react.res 1`] = ` ReactDOMRe.createDOMElementVariadic(\\"div\\", []) } let component = { - let \\\\\\"InnerModule.react$Bar$component\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => + let \\\\\\"InnerModule$Bar$component\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => component(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ()) - \\\\\\"InnerModule.react$Bar$component\\" + \\\\\\"InnerModule$Bar$component\\" } } " `; -exports[`topLevel.react.res 1`] = ` +exports[`topLevel.res 1`] = ` "@bs.obj external makeProps: ( ~a: 'a, @@ -71,9 +70,9 @@ let make = ReactDOMRe.createDOMElementVariadic(\\"div\\", []) } let make = { - let \\\\\\"TopLevel.react\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => + let \\\\\\"TopLevel\\" = (\\\\\\"Props\\": {\\"a\\": 'a, \\"b\\": 'b}) => make(~b=\\\\\\"Props\\"[\\"b\\"], ~a=\\\\\\"Props\\"[\\"a\\"], ()) - \\\\\\"TopLevel.react\\" + \\\\\\"TopLevel\\" } " `; diff --git a/tests/printer/reactJsx/commentAtTop.react.res b/tests/printer/reactJsx/commentAtTop.res similarity index 100% rename from tests/printer/reactJsx/commentAtTop.react.res rename to tests/printer/reactJsx/commentAtTop.res diff --git a/tests/printer/reactJsx/externalWithCustomName.react.res.fixme b/tests/printer/reactJsx/externalWithCustomName.res.fixme similarity index 100% rename from tests/printer/reactJsx/externalWithCustomName.react.res.fixme rename to tests/printer/reactJsx/externalWithCustomName.res.fixme diff --git a/tests/printer/reactJsx/innerModule.react.res b/tests/printer/reactJsx/innerModule.res similarity index 100% rename from tests/printer/reactJsx/innerModule.react.res rename to tests/printer/reactJsx/innerModule.res diff --git a/tests/printer/reactJsx/topLevel.react.res b/tests/printer/reactJsx/topLevel.res similarity index 100% rename from tests/printer/reactJsx/topLevel.react.res rename to tests/printer/reactJsx/topLevel.res From dd2f6809c4ba24cc80793d82de4ab71e30ee57aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cha=CC=81varri?= Date: Tue, 22 Sep 2020 00:23:20 +0200 Subject: [PATCH 5/8] remove unneeded jsx stuff from res_multi_printer --- .depend | 2 +- src/res_multi_printer.ml | 37 +------------------------------------ src/res_multi_printer.mli | 2 +- tests/res_test.ml | 25 ------------------------- 4 files changed, 3 insertions(+), 63 deletions(-) diff --git a/.depend b/.depend index ef2590a6..ac5048af 100644 --- a/.depend +++ b/.depend @@ -47,7 +47,7 @@ src/res_minibuffer.cmi : src/res_multi_printer.cmx : src/res_printer.cmx src/res_io.cmx \ src/res_driver_reason_binary.cmx src/res_driver_ml_parser.cmx \ src/res_driver.cmx src/res_diagnostics.cmx src/res_ast_conversion.cmx \ - src/reactjs_jsx_ppx.cmx src/res_multi_printer.cmi + src/res_multi_printer.cmi src/res_multi_printer.cmi : src/res_outcome_printer.cmx : src/res_token.cmx src/res_doc.cmx \ src/res_outcome_printer.cmi diff --git a/src/res_multi_printer.ml b/src/res_multi_printer.ml index 220f1071..cfcf1942 100644 --- a/src/res_multi_printer.ml +++ b/src/res_multi_printer.ml @@ -51,40 +51,6 @@ let printMl ~isInterface ~filename = ~comments:parseResult.comments parseResult.parsetree -(* print files using React JSX ppx to res syntax *) -let printJsx ~isInterface ~filename = - if isInterface then - let parseResult = - Res_driver.parsingEngine.parseInterface ~forPrinter:true ~filename - in - if parseResult.invalid then - begin - Res_diagnostics.printReport parseResult.diagnostics parseResult.source; - exit 1 - end - else - let jsxPass = Reactjs_jsx_ppx.rewrite_signature parseResult.parsetree in - Res_printer.printInterface - ~width:defaultPrintWidth - ~comments:parseResult.comments - jsxPass - else - let parseResult = - Res_driver.parsingEngine.parseImplementation ~forPrinter:true ~filename - in - if parseResult.invalid then - begin - Res_diagnostics.printReport parseResult.diagnostics parseResult.source; - exit 1 - end - else - let jsxPass = Reactjs_jsx_ppx.rewrite_implementation parseResult.parsetree in - Res_printer.printImplementation - ~width:defaultPrintWidth - ~comments:parseResult.comments - jsxPass -[@@raises Invalid_argument, Failure, exit] - (* How does printing Reason to Res work? * -> open a tempfile * -> write the source code found in "filename" into the tempfile @@ -156,8 +122,7 @@ let print language ~input = len > 0 && String.unsafe_get input (len - 1) = 'i' in match language with - | `jsx -> printJsx ~isInterface ~filename:input | `res -> printRes ~isInterface ~filename:input | `ml -> printMl ~isInterface ~filename:input | `refmt path -> printReason ~refmtPath:path ~isInterface ~filename:input -[@@raises Invalid_argument, Failure, Sys_error, exit] +[@@raises Sys_error, exit] diff --git a/src/res_multi_printer.mli b/src/res_multi_printer.mli index 8e5f8ca2..1a1d9624 100644 --- a/src/res_multi_printer.mli +++ b/src/res_multi_printer.mli @@ -1,3 +1,3 @@ (* Interface to print source code from different languages to res. * Takes a filename called "input" and returns the corresponding formatted res syntax *) -val print: [`jsx | `ml | `res | `refmt of string (* path to refmt *)] -> input: string -> string +val print: [`ml | `res | `refmt of string (* path to refmt *)] -> input: string -> string diff --git a/tests/res_test.ml b/tests/res_test.ml index 2b62c8e8..b3ab7dd0 100644 --- a/tests/res_test.ml +++ b/tests/res_test.ml @@ -126,31 +126,6 @@ let y: float |} ) -(* test printing of React jsx file *) -let () = -let filename = "./tests/api/resReactJsx.res" in -let prettySource = Res_multi_printer.print `jsx ~input:filename in -assert ( - prettySource = {|@bs.obj -external makeProps: ( - ~msg: 'msg, - ~key: string=?, - unit, -) => { - "msg": 'msg, - // test React JSX file -} = "" - -let make = - (@warning("-16") ~msg) => { - ReactDOMRe.createDOMElementVariadic("div", [{msg->React.string}]) - } -let make = { - let \"ResReactJsx" = (\"Props": {"msg": 'msg}) => make(~msg=\"Props"["msg"]) - \"ResReactJsx" -} -|}) - let () = print_endline "✅ multi printer api tests" module OutcomePrinterTests = struct From e84640a4850732c552107d531dd1c32f6da0549d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cha=CC=81varri?= Date: Tue, 22 Sep 2020 00:27:14 +0200 Subject: [PATCH 6/8] remove .fixme option and add previously failing test + snapshot --- .../__snapshots__/render.spec.js.snap | 23 +++++++++++++++++++ ...e.res.fixme => externalWithCustomName.res} | 0 tests/runner.js | 7 ------ 3 files changed, 23 insertions(+), 7 deletions(-) rename tests/printer/reactJsx/{externalWithCustomName.res.fixme => externalWithCustomName.res} (100%) diff --git a/tests/printer/reactJsx/__snapshots__/render.spec.js.snap b/tests/printer/reactJsx/__snapshots__/render.spec.js.snap index ddca8f7e..34daac1b 100644 --- a/tests/printer/reactJsx/__snapshots__/render.spec.js.snap +++ b/tests/printer/reactJsx/__snapshots__/render.spec.js.snap @@ -15,6 +15,29 @@ let make = { " `; +exports[`externalWithCustomName.res 1`] = ` +"module Foo = { + @bs.obj + external componentProps: ( + ~a: int, + ~b: string, + ~key: string=?, + unit, + ) => {\\"a\\": int, \\"b\\": string} = \\"\\" + @bs.module(\\"Foo\\") + external component: React.componentLike< + {\\"a\\": int, \\"b\\": string}, + React.element, + > = \\"component\\" +} + +let t = React.createElement( + Foo.component, + Foo.componentProps(~a=1, ~b={\\"1\\"}, ()), +) +" +`; + exports[`innerModule.res 1`] = ` "module Bar = { @bs.obj diff --git a/tests/printer/reactJsx/externalWithCustomName.res.fixme b/tests/printer/reactJsx/externalWithCustomName.res similarity index 100% rename from tests/printer/reactJsx/externalWithCustomName.res.fixme rename to tests/printer/reactJsx/externalWithCustomName.res diff --git a/tests/runner.js b/tests/runner.js index cfeff587..ce56cc39 100644 --- a/tests/runner.js +++ b/tests/runner.js @@ -206,13 +206,6 @@ global.runPrinter = (dirname, ppx = "none") => { fs.readdirSync(dirname).forEach((base) => { let filename = path.join(dirname, base); - if (filename.length > 6) { - let suffix = filename.substring(filename.length - 6); - if (suffix === ".fixme") { - return; - } - } - if (!fs.lstatSync(filename).isFile() || base === "render.spec.js") { return; } From 739a1dfb5f41e7eff858b285f93269f1e2d5c037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cha=CC=81varri?= Date: Tue, 22 Sep 2020 00:33:33 +0200 Subject: [PATCH 7/8] don't run roundtrip tests if ppx is defined --- tests/runner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/runner.js b/tests/runner.js index ce56cc39..93a475b6 100644 --- a/tests/runner.js +++ b/tests/runner.js @@ -225,7 +225,7 @@ Make sure the test input is syntactically valid.`; expect(result).toMatchSnapshot(); } - if (process.env.ROUNDTRIP_TEST) { + if (process.env.ROUNDTRIP_TEST && ppx === "none") { let intf = isInterface(filename); let sexpAst = parseFileToSexp(filename); let result2 = parseNapkinStdinToNapkin(result, intf, 80); From 23336db4e0b7d9f524db573b4e94afe510db43ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Cha=CC=81varri?= Date: Tue, 22 Sep 2020 22:57:29 +0200 Subject: [PATCH 8/8] move ppx tests to own folder, remove "none" as default arg value, cleanup --- Makefile | 4 +--- src/res_cli.ml | 15 +++++---------- .../react}/__snapshots__/render.spec.js.snap | 0 .../reactJsx => ppx/react}/commentAtTop.res | 0 .../react}/externalWithCustomName.res | 0 .../reactJsx => ppx/react}/innerModule.res | 0 .../reactJsx => ppx/react}/render.spec.js | 0 .../{printer/reactJsx => ppx/react}/topLevel.res | 0 tests/runner.js | 6 ++++-- 9 files changed, 10 insertions(+), 15 deletions(-) rename tests/{printer/reactJsx => ppx/react}/__snapshots__/render.spec.js.snap (100%) rename tests/{printer/reactJsx => ppx/react}/commentAtTop.res (100%) rename tests/{printer/reactJsx => ppx/react}/externalWithCustomName.res (100%) rename tests/{printer/reactJsx => ppx/react}/innerModule.res (100%) rename tests/{printer/reactJsx => ppx/react}/render.spec.js (100%) rename tests/{printer/reactJsx => ppx/react}/topLevel.res (100%) diff --git a/Makefile b/Makefile index 85941067..e76acc34 100644 --- a/Makefile +++ b/Makefile @@ -50,8 +50,6 @@ lib/rescript.exe: $(CLI_FILES) build-native: lib/refmt.exe lib/rescript.exe depend -build-ppx: src/reactjs_jsx_ppx.cmx - bootstrap: build-native ocaml unix.cma ./scripts/bspack.ml -bs-main Res_cli -I src -o ./lib/rescript.ml ./lib/rescript.exe -parse ml -print ns ./lib/Rescript.ml > ./lib/Rescript2.ml @@ -97,4 +95,4 @@ clean: rm -rf lib/rescript.exe rm -rf lib/test.exe git clean -dfx src -.PHONY: clean test roundtrip-test termination dce exception reanalyze bootstrap build-native build-ppx +.PHONY: clean test roundtrip-test termination dce exception reanalyze bootstrap build-native diff --git a/src/res_cli.ml b/src/res_cli.ml index 66ab57d2..59839ff8 100644 --- a/src/res_cli.ml +++ b/src/res_cli.ml @@ -179,7 +179,7 @@ end = struct let origin = ref "" let interface = ref false let report = ref "pretty" - let ppx = ref "none" + let ppx = ref "" let usage = "Usage:\n rescript \n\n" ^ "Examples:\n" ^ @@ -230,11 +230,6 @@ module CliArgProcessor = struct | _ -> false in - let ppx = match ppx with - | "jsx" -> `Jsx - | _ -> `None - in - let Parser backend = parsingEngine in (* This is the whole purpose of the Color module above *) Color.setup None; @@ -252,8 +247,8 @@ module CliArgProcessor = struct end else let parsetree = match ppx with - | `Jsx -> Reactjs_jsx_ppx.rewrite_signature parseResult.parsetree - | `None -> parseResult.parsetree + | "jsx" -> Reactjs_jsx_ppx.rewrite_signature parseResult.parsetree + | _ -> parseResult.parsetree in printEngine.printInterface ~width ~filename ~comments:parseResult.comments parsetree @@ -271,8 +266,8 @@ module CliArgProcessor = struct end else let parsetree = match ppx with - | `Jsx -> Reactjs_jsx_ppx.rewrite_implementation parseResult.parsetree - | `None -> parseResult.parsetree + | "jsx" -> Reactjs_jsx_ppx.rewrite_implementation parseResult.parsetree + | _ -> parseResult.parsetree in printEngine.printImplementation ~width ~filename ~comments:parseResult.comments parsetree diff --git a/tests/printer/reactJsx/__snapshots__/render.spec.js.snap b/tests/ppx/react/__snapshots__/render.spec.js.snap similarity index 100% rename from tests/printer/reactJsx/__snapshots__/render.spec.js.snap rename to tests/ppx/react/__snapshots__/render.spec.js.snap diff --git a/tests/printer/reactJsx/commentAtTop.res b/tests/ppx/react/commentAtTop.res similarity index 100% rename from tests/printer/reactJsx/commentAtTop.res rename to tests/ppx/react/commentAtTop.res diff --git a/tests/printer/reactJsx/externalWithCustomName.res b/tests/ppx/react/externalWithCustomName.res similarity index 100% rename from tests/printer/reactJsx/externalWithCustomName.res rename to tests/ppx/react/externalWithCustomName.res diff --git a/tests/printer/reactJsx/innerModule.res b/tests/ppx/react/innerModule.res similarity index 100% rename from tests/printer/reactJsx/innerModule.res rename to tests/ppx/react/innerModule.res diff --git a/tests/printer/reactJsx/render.spec.js b/tests/ppx/react/render.spec.js similarity index 100% rename from tests/printer/reactJsx/render.spec.js rename to tests/ppx/react/render.spec.js diff --git a/tests/printer/reactJsx/topLevel.res b/tests/ppx/react/topLevel.res similarity index 100% rename from tests/printer/reactJsx/topLevel.res rename to tests/ppx/react/topLevel.res diff --git a/tests/runner.js b/tests/runner.js index 93a475b6..c6256bdf 100644 --- a/tests/runner.js +++ b/tests/runner.js @@ -202,7 +202,7 @@ let makeReproducibleFilename = (txt) => { }) }; -global.runPrinter = (dirname, ppx = "none") => { +global.runPrinter = (dirname, ppx = "") => { fs.readdirSync(dirname).forEach((base) => { let filename = path.join(dirname, base); @@ -225,7 +225,9 @@ Make sure the test input is syntactically valid.`; expect(result).toMatchSnapshot(); } - if (process.env.ROUNDTRIP_TEST && ppx === "none") { + // Only run roundtrip tests in ppx-free tests. + // Ppxs are only applied in .res syntax, not .re, so resulting ASTs would not match + if (process.env.ROUNDTRIP_TEST && ppx === "") { let intf = isInterface(filename); let sexpAst = parseFileToSexp(filename); let result2 = parseNapkinStdinToNapkin(result, intf, 80);