diff --git a/newtests/csx/test.js b/newtests/csx/test.js index 844d0f2b923..3e4959ed0e8 100644 --- a/newtests/csx/test.js +++ b/newtests/csx/test.js @@ -84,6 +84,104 @@ export default suite(({addFile, addFiles, addCode}) => [ `) .noNewErrors(), ]), + test('Should raise no errors if CSX children are passed as spread with list inline', [ + addCode(` + // @csx + type Props = {|children: Array|}; + function Foo(props: Props) {} + {...["foo", "bar"]}; + `) + .noNewErrors(), + ]), + test('Should raise no errors if CSX children are passed as spread with variable', [ + addCode(` + // @csx + type Props = {|children: Array|}; + function Foo(props: Props) {} + const arr = ["foo", "bar"]; + {...arr}; + `) + .noNewErrors(), + ]), + test('Should raise an error if CSX spread children are not a list', [ + addCode(` + // @csx + type Props = {|children: Array|}; + function Foo(props: Props) {} + const x = 42; + {...x}; + `) + .newErrors( + ` + test.js:8 + 8: {...x}; + ^ number. This type is incompatible with + 8: {...x}; + ^ $Iterable + Property \`@@iterator\` is incompatible: + 8: {...x}; + ^ property \`@@iterator\` of $Iterable. Property not found in + 8: {...x}; + ^ number + ` + ) + ]), + test('Should raise an error if CSX children passed as spread have the wrong type', [ + addCode(` + // @csx + type Props = {|children: Array|}; + function Foo(props: Props) {} + const arr = ["foo"]; + {...arr}; + `) + .newErrors( + ` + test.js:8 + 8: {...arr}; + ^^^^^ props of JSX element \`Foo\`. This type is incompatible with the expected param type of + 6: function Foo(props: Props) {} + ^^^^^ object type + Property \`children\` is incompatible: + 8: {...arr}; + ^^^^^ JSX element \`Foo\`. Has some incompatible type argument with + 5: type Props = {|children: Array|}; + ^^^^^^^^^^^^^ array type + Type argument \`T\` is incompatible: + 7: const arr = ["foo"]; + ^^^^^ string. This type is incompatible with + 5: type Props = {|children: Array|}; + ^^^^^^ number + `, + ), + ]), + test('Should raise no errors if CSX children are passed as spread with function call', [ + addCode(` + // @csx + type Props = {|children: Array|}; + type TProps = {|text: string|}; + function Foo(props: Props) {} + function Title(props: TProps) { return 42; } + function get_titles(l) { + return []; + } + var arr = ["foo", "bar"]; + {...get_titles(arr)}; + `) + .noNewErrors(), + ]), + test('Should raise no errors if CSX children are passed as spread with other children', [ + addCode(` + // @csx + type Props = {|children: Array|}; + function Foo(props: Props) {} + + {"foobar"} + {...["foo", "bar"]} + {"baz"} + ; + `) + .noNewErrors(), + ]), test('Should raise no errors if a JSX spread provides all required attributes for an exact type', [ addCode(` // @csx diff --git a/newtests/jsx_pragma/test.js b/newtests/jsx_pragma/test.js index 2bfe43b19e0..6525fed69d5 100644 --- a/newtests/jsx_pragma/test.js +++ b/newtests/jsx_pragma/test.js @@ -301,6 +301,15 @@ export default suite(({addFile, addFiles, addCode}) => [ ; `).noNewErrors(), ]), + test('Spread syntax in children should work', [ + addCode(` + // @jsx Foo + function Foo(elem: number, props: null, child1: 'a', child2: 'b', child3: 'c') {} + + const Bar = 123; + {...["a", "b", "c"]}; + `).noNewErrors(), + ]), test('Exact prop type with spread still does not work', [ addCode(` // @jsx Foo diff --git a/src/parser/ast.ml b/src/parser/ast.ml index 90767ae7377..59571e43ca8 100644 --- a/src/parser/ast.ml +++ b/src/parser/ast.ml @@ -906,6 +906,7 @@ and JSX : sig | Element of 'M element | Fragment of 'M fragment | ExpressionContainer of 'M ExpressionContainer.t + | SpreadChild of 'M Expression.t | Text of Text.t and 'M element = { diff --git a/src/parser/estree_translator.ml b/src/parser/estree_translator.ml index 20b24508301..53475123db0 100644 --- a/src/parser/estree_translator.ml +++ b/src/parser/estree_translator.ml @@ -1289,6 +1289,10 @@ end with type t = Impl.t) = struct | loc, Element element -> jsx_element (loc, element) | loc, Fragment fragment -> jsx_fragment (loc, fragment) | loc, ExpressionContainer expr -> jsx_expression_container (loc, expr) + | loc, SpreadChild expr -> + node "JSXSpreadChild" loc [| + "expression", expression expr; + |] | loc, Text str -> jsx_text (loc, str) ) diff --git a/src/parser/jsx_parser.ml b/src/parser/jsx_parser.ml index ef6db723e05..c512ff04fac 100644 --- a/src/parser/jsx_parser.ml +++ b/src/parser/jsx_parser.ml @@ -24,10 +24,7 @@ module JSX (Parse: Parser_common.PARSER) = struct argument; }) - let expression_container env = - Eat.push_lex_mode env Lex_mode.NORMAL; - let start_loc = Peek.loc env in - Expect.token env T_LCURLY; + let expression_container' env start_loc = let expression = if Peek.token env = T_RCURLY then let empty_loc = Loc.btwn_exclusive start_loc (Peek.loc env) in @@ -40,6 +37,28 @@ module JSX (Parse: Parser_common.PARSER) = struct expression; }) + let expression_container env = + Eat.push_lex_mode env Lex_mode.NORMAL; + let start_loc = Peek.loc env in + Expect.token env T_LCURLY; + expression_container' env start_loc + + let expression_container_or_spread_child env = + Eat.push_lex_mode env Lex_mode.NORMAL; + let start_loc = Peek.loc env in + Expect.token env T_LCURLY; + match Peek.token env with + | T_ELLIPSIS -> + Expect.token env T_ELLIPSIS; + let expr = Parse.assignment env in + let end_loc = Peek.loc env in + Expect.token env T_RCURLY; + Eat.pop_lex_mode env; + Loc.btwn start_loc end_loc, JSX.SpreadChild expr + | _ -> + let expression_container = expression_container' env start_loc in + fst expression_container, JSX.ExpressionContainer (snd expression_container) + let identifier env = let loc = Peek.loc env in let name = match Peek.token env with @@ -195,12 +214,9 @@ module JSX (Parse: Parser_common.PARSER) = struct | ChildElement of (Loc.t * Loc.t JSX.element) | ChildFragment of (Loc.t * Loc.t JSX.fragment) - let rec child env = match Peek.token env with - | T_LCURLY -> - let expression_container = expression_container env in - fst expression_container, JSX.ExpressionContainer (snd expression_container) + | T_LCURLY -> expression_container_or_spread_child env | T_JSX_TEXT (loc, value, raw) as token -> Expect.token env token; loc, JSX.Text { JSX.Text.value; raw; } diff --git a/src/parser_utils/flow_ast_mapper.ml b/src/parser_utils/flow_ast_mapper.ml index cfbf064aa43..e031b4df5cb 100644 --- a/src/parser_utils/flow_ast_mapper.ml +++ b/src/parser_utils/flow_ast_mapper.ml @@ -800,6 +800,8 @@ class mapper = object(this) id this#jsx_fragment frag child (fun frag -> loc, Fragment frag) | ExpressionContainer expr -> id this#jsx_expression expr child (fun expr -> loc, ExpressionContainer expr) + | SpreadChild expr -> + id this#expression expr child (fun expr -> loc, SpreadChild expr) | Text _ -> child method jsx_expression (jsx_expr: Loc.t Ast.JSX.ExpressionContainer.t) = diff --git a/src/typing/statement.ml b/src/typing/statement.ml index f1b34db845b..8d43d8d6b95 100644 --- a/src/typing/statement.ml +++ b/src/typing/statement.ml @@ -3703,7 +3703,7 @@ and jsx_mk_props cx reason c name attributes children = Ast.JSX.( cx ~use_op:UnknownUse ~reason_op:reason - (List.map (fun child -> UnresolvedArg child) children) + children (ResolveSpreadsToArrayLiteral (mk_id (), tout)) ) in let p = Field (None, arr, Neutral) in @@ -3717,6 +3717,13 @@ and jsx_desugar cx name component_t props attributes children eloc = | None -> let reason = mk_reason (RReactElement (Some name)) eloc in let react = Env.var_ref ~lookup_mode:ForValue cx "React" eloc in + let children = List.map (function + | UnresolvedArg a -> a + | UnresolvedSpreadArg a -> + (* TODO - the location might be wrong here? *) + Flow.add_output cx Flow_error.(EUnsupportedSyntax (eloc, SpreadArgument)); + AnyT.why (reason_of_t a) + ) children in Tvar.mk_where cx reason (fun tvar -> let reason_createElement = mk_reason (RProperty (Some "createElement")) eloc in @@ -3740,7 +3747,10 @@ and jsx_desugar cx name component_t props attributes children eloc = | _ -> props in let argts = [Arg component_t; Arg props] @ - (List.map (fun c -> Arg c) children) in + (List.map (function + | UnresolvedArg c -> Arg c + | UnresolvedSpreadArg c -> SpreadArg c + ) children) in Ast.Expression.(match jsx_expr with | _, Member { Member._object; @@ -3783,17 +3793,19 @@ and jsx_pragma_expression cx raw_jsx_expr loc = Ast.Expression.(function ) and jsx_body cx = Ast.JSX.(function - | _, Element e -> Some (jsx cx e) - | _, Fragment f -> Some (jsx_fragment cx f) + | _, Element e -> Some (UnresolvedArg (jsx cx e)) + | _, Fragment f -> Some (UnresolvedArg (jsx_fragment cx f)) | _, ExpressionContainer ec -> ( let open ExpressionContainer in let { expression = ex } = ec in - Some (match ex with + Some (UnresolvedArg (match ex with | Expression (loc, e) -> expression cx (loc, e) | EmptyExpression loc -> - DefT (mk_reason (RCustom "empty jsx body") loc, EmptyT)) + DefT (mk_reason (RCustom "empty jsx body") loc, EmptyT))) ) - | loc, Text { Text.value; raw=_; } -> jsx_trim_text loc value + | _, SpreadChild expr -> Some (UnresolvedSpreadArg (expression cx expr)) + | loc, Text { Text.value; raw=_; } -> + Option.map (jsx_trim_text loc value) (fun c -> UnresolvedArg c) ) and jsx_trim_text loc value = diff --git a/tests/react_children/react_children.exp b/tests/react_children/react_children.exp index cbce09c2036..ecf78d5a4ed 100644 --- a/tests/react_children/react_children.exp +++ b/tests/react_children/react_children.exp @@ -878,6 +878,10 @@ Error: fun.js:142 21: class FunArray extends React.Component<{children: Fn | Array}, void> {} ^^^^^^^^^ array type +Error: spread.js:7 + 7: {...["a", "b"]} + ^^^^^ A spread argument is unsupported here + Error: tabs.js:26 26: ; // Error: `children` is required. ^^^^^^^^^^ props of React element `TabBar`. This type is incompatible with @@ -2433,4 +2437,4 @@ Error: view.js:40 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ array type -Found 111 errors +Found 112 errors diff --git a/tests/react_children/spread.js b/tests/react_children/spread.js new file mode 100644 index 00000000000..74f3b549065 --- /dev/null +++ b/tests/react_children/spread.js @@ -0,0 +1,7 @@ +// @flow + +import React from 'react'; + +class Foo extends React.Component<{children: Array}, void> {} + +{...["a", "b"]}