Skip to content

Commit

Permalink
Parse this constraints in functions
Browse files Browse the repository at this point in the history
Summary:
Implements basic parser support for specifying `this` type constraints in functions and function types. The syntax is similar to [`this` parameters in TypeScript](https://www.typescriptlang.org/docs/handbook/functions.html#this-parameters).

## Motivating use case

NOTE: This is a hypothetical example. This diff does **not** implement any typechecking semantics.

With this syntax, typing `Array.prototype.join` as

```
declare class $ReadOnlyArray<T> {
  join(this: $ArrayLike<string>, separator?: string): string;
}
```

could make it an error to call `join` with a non-string element type, e.g. `[{}].join()`, thus guarding against the implicit string coercion inside the method.

As a side benefit, it would allow `join` to be safely used on array-like objects that do not extend `Array`, which is explicitly allowed by [the spec](https://tc39.es/ecma262/#sec-array.prototype.join):

> The `join` function is intentionally generic; it does not require that its `this` value be an Array object. Therefore, it can be transferred to other kinds of objects for use as a method.

The spec uses similar "intentionally generic" language on many other built-in methods, providing other natural opportunities to use this feature in the core libdefs.

Reviewed By: gkz

Differential Revision: D22276547

fbshipit-source-id: b0cd0a921582e29bf29ec414b8c386cd411dda7b
  • Loading branch information
dsainati1 authored and facebook-github-bot committed Oct 13, 2020
1 parent bc68f94 commit 8efdc8c
Show file tree
Hide file tree
Showing 98 changed files with 6,020 additions and 154 deletions.
5 changes: 5 additions & 0 deletions packages/flow-parser/test/esprima_test_runner.js
Expand Up @@ -197,6 +197,11 @@ function handleSpecialObjectCompare(esprima, flow, env) {
esprima.typeAnnotation = null;
}
break;
case 'FunctionTypeAnnotation':
if (!esprima.hasOwnProperty('this')) {
esprima.this = null;
}
break;
case 'ObjectTypeAnnotation':
esprima.exact = esprima.exact || false;
delete flow.inexact;
Expand Down
1 change: 1 addition & 0 deletions src/codemods/annotate_exports.ml
Expand Up @@ -409,6 +409,7 @@ let mapper ~preserve_literals ~max_type_size ~default_any (cctx : Codemod_contex
} );
];
rest = None;
this_ = None;
comments = _;
} );
return = Ast.Type.Missing rloc;
Expand Down
9 changes: 8 additions & 1 deletion src/common/ty/ty_serializer.ml
Expand Up @@ -150,7 +150,14 @@ let type_ options =
and fun_params params rest_param =
let%bind params = mapM fun_param params in
let%map rest = opt fun_rest_param rest_param in
(Loc.none, { T.Function.Params.params; rest; comments = None })
( Loc.none,
{
T.Function.Params.params;
rest;
(* TODO: handle `this` constraints *)
this_ = None;
comments = None;
} )
and fun_param (name, t, { prm_optional }) =
let name = Base.Option.map ~f:id_from_string name in
let%map annot = type_ t in
Expand Down
10 changes: 10 additions & 0 deletions src/parser/comment_attachment.ml
Expand Up @@ -706,6 +706,11 @@ let function_rest_param_comment_bounds (loc, param) =
ignore (collector#function_rest_param (loc, param));
collect_without_trailing_line_comment collector

let function_this_param_comment_bounds (loc, param) =
let collector = new comment_bounds_collector ~loc in
ignore (collector#function_this_param (loc, param));
collect_without_trailing_line_comment collector

let function_type_param_comment_bounds (loc, param) =
let collector = new comment_bounds_collector ~loc in
ignore (collector#function_param_type (loc, param));
Expand All @@ -716,6 +721,11 @@ let function_type_rest_param_comment_bounds (loc, param) =
ignore (collector#function_rest_param_type (loc, param));
collect_without_trailing_line_comment collector

let function_type_this_param_comment_bounds (loc, param) =
let collector = new comment_bounds_collector ~loc in
ignore (collector#function_this_param_type (loc, param));
collect_without_trailing_line_comment collector

let array_element_comment_bounds loc element =
let collector = new comment_bounds_collector ~loc in
ignore (collector#array_element element);
Expand Down
28 changes: 26 additions & 2 deletions src/parser/declaration_parser.ml
Expand Up @@ -110,7 +110,7 @@ module Declaration (Parse : Parser_common.PARSER) (Type : Type_parser.TYPE) : DE
* strict mode due to a directive in the function.
* Simple is the IsSimpleParameterList thing from the ES6 spec *)
let strict_post_check
env ~strict ~simple id (_, { Ast.Function.Params.params; rest; comments = _ }) =
env ~strict ~simple id (_, { Ast.Function.Params.params; rest; this_ = _; comments = _ }) =
if strict || not simple then (
(* If we are doing this check due to strict mode than there are two
* cases to consider. The first is when we were already in strict mode
Expand Down Expand Up @@ -144,6 +144,7 @@ module Declaration (Parse : Parser_common.PARSER) (Type : Type_parser.TYPE) : DE
let function_params =
let rec param =
with_loc (fun env ->
if Peek.token env = T_THIS then error env Parse_error.ThisParamMustBeFirst;
let argument = Parse.pattern env Parse_error.StrictParamName in
let default =
if Peek.token env = T_ASSIGN then (
Expand Down Expand Up @@ -182,6 +183,27 @@ module Declaration (Parse : Parser_common.PARSER) (Type : Type_parser.TYPE) : DE
if Peek.token env <> T_RPAREN then Expect.token env T_COMMA;
param_list env (the_param :: acc)
in
let this_param_annotation env =
if should_parse_types env && Peek.token env = T_THIS then (
let (this_loc, this_param) =
with_loc
(fun env ->
Expect.token env T_THIS;
if Peek.token env <> T_COLON then begin
error env Parse_error.ThisParamAnnotationRequired;
None
end else
Some (Type.annotation env))
env
in
match this_param with
| None -> None
| Some this_param ->
if Peek.token env = T_COMMA then Eat.token env;
Some (this_loc, this_param)
) else
None
in
fun ~await ~yield ->
with_loc (fun env ->
let env =
Expand All @@ -192,6 +214,7 @@ module Declaration (Parse : Parser_common.PARSER) (Type : Type_parser.TYPE) : DE
in
let leading = Peek.comments env in
Expect.token env T_LPAREN;
let this_ = this_param_annotation env in
let (params, rest) = param_list env [] in
let internal = Peek.comments env in
Expect.token env T_RPAREN;
Expand All @@ -200,6 +223,7 @@ module Declaration (Parse : Parser_common.PARSER) (Type : Type_parser.TYPE) : DE
Ast.Function.Params.params;
rest;
comments = Flow_ast_utils.mk_comments_with_internal_opt ~leading ~trailing ~internal;
this_;
})

let function_body env ~async ~generator ~expression =
Expand Down Expand Up @@ -258,7 +282,7 @@ module Declaration (Parse : Parser_common.PARSER) (Type : Type_parser.TYPE) : DE
| (_, { Ast.Function.Param.argument = (_, Pattern.Identifier _); default = None }) -> true
| _ -> false
in
fun (_, { Ast.Function.Params.params; rest; comments = _ }) ->
fun (_, { Ast.Function.Params.params; rest; comments = _; this_ = _ }) ->
rest = None && List.for_all is_simple_param params

let _function =
Expand Down
31 changes: 29 additions & 2 deletions src/parser/estree_translator.ml
Expand Up @@ -1155,6 +1155,11 @@ with type t = Impl.t = struct
| Some default ->
node "AssignmentPattern" loc [("left", pattern argument); ("right", expression default)]
| None -> pattern argument
and this_param (loc, annotation) =
node
"Identifier"
loc
[("name", string "this"); ("typeAnnotation", type_annotation annotation)]
and function_params =
let open Ast.Function.Params in
function
Expand All @@ -1163,12 +1168,25 @@ with type t = Impl.t = struct
params;
rest = Some (rest_loc, { Function.RestParam.argument; comments });
comments = _;
this_;
} ) ->
let rest = node ?comments "RestElement" rest_loc [("argument", pattern argument)] in
let rev_params = List.rev_map function_param params in
let params = List.rev (rest :: rev_params) in
let params =
match this_ with
| Some this -> this_param this :: params
| None -> params
in
array params
| (_, { params; rest = None; this_; comments = _ }) ->
let params = List.map function_param params in
let params =
match this_ with
| Some this -> this_param this :: params
| None -> params
in
array params
| (_, { params; rest = None; comments = _ }) -> array_of_list function_param params
and rest_element loc { Pattern.RestElement.argument; comments } =
node ?comments "RestElement" loc [("argument", pattern argument)]
and array_pattern_element =
Expand Down Expand Up @@ -1382,7 +1400,7 @@ with type t = Impl.t = struct
( loc,
{
Type.Function.params =
(_, { Type.Function.Params.params; rest; comments = params_comments });
(_, { Type.Function.Params.this_; params; rest; comments = params_comments });
return;
tparams;
comments = func_comments;
Expand All @@ -1398,6 +1416,7 @@ with type t = Impl.t = struct
loc
[
("params", array_of_list function_type_param params);
("this", option function_type_this_constraint this_);
("returnType", _type return);
("rest", option function_type_rest rest);
("typeParameters", option type_parameter_declaration tparams);
Expand All @@ -1421,6 +1440,14 @@ with type t = Impl.t = struct
"argument", function_type_param argument;
] *)
function_type_param ?comments argument
and function_type_this_constraint (loc, { Type.Function.ThisParam.annot; comments }) =
node
?comments
"FunctionTypeParam"
loc
[
("name", option identifier None); ("typeAnnotation", _type annot); ("optional", bool false);
]
and object_type ~include_inexact (loc, { Type.Object.properties; exact; inexact; comments }) =
Type.Object.(
let (props, ixs, calls, slots) =
Expand Down
26 changes: 22 additions & 4 deletions src/parser/expression_parser.ml
Expand Up @@ -175,6 +175,7 @@ module Expression
| (T_YIELD, _) when allow_yield env -> Cover_expr (yield env)
| ((T_LPAREN as t), _)
| ((T_LESS_THAN as t), _)
| ((T_THIS as t), _)
| (t, true) ->
(* Ok, we don't know if this is going to be an arrow function or a
* regular assignment expression. Let's first try to parse it as an
Expand Down Expand Up @@ -1518,7 +1519,8 @@ module Expression
| StrictReservedWord
| ParameterAfterRestParameter
| NewlineBeforeArrow
| YieldInFormalParameters ->
| YieldInFormalParameters
| ThisParamBannedInArrowFunctions ->
()
(* Everything else causes a rollback *)
| _ -> raise Try.Rollback)
Expand Down Expand Up @@ -1569,7 +1571,13 @@ module Expression
} )
in
( tparams,
(loc, { Ast.Function.Params.params = [param]; rest = None; comments = None }),
( loc,
{
Ast.Function.Params.params = [param];
rest = None;
comments = None;
this_ = None;
} ),
Ast.Type.Missing Loc.{ loc with start = loc._end },
None )
else
Expand All @@ -1596,11 +1604,21 @@ module Expression
* instead generate errors as if we were parsing an arrow function *)
let env =
match params with
| (_, { Ast.Function.Params.rest = Some _; _ })
| (_, { Ast.Function.Params.params = []; _ }) ->
| (_, { Ast.Function.Params.params = _; rest = Some _; this_ = None; comments = _ })
| (_, { Ast.Function.Params.params = []; rest = _; this_ = None; comments = _ }) ->
without_error_callback env
| _ -> env
in

(* Disallow this param annotations in arrow functions *)
let params =
match params with
| (loc, ({ Ast.Function.Params.this_ = Some (this_loc, _); _ } as params)) ->
error_at env (this_loc, Parse_error.ThisParamBannedInArrowFunctions);
(loc, { params with Ast.Function.Params.this_ = None })
| _ -> params
in

if Peek.is_line_terminator env && Peek.token env = T_ARROW then
error env Parse_error.NewlineBeforeArrow;
Expect.token env T_ARROW;
Expand Down
12 changes: 12 additions & 0 deletions src/parser/flow_ast.ml
Expand Up @@ -159,10 +159,21 @@ and Type : sig
[@@deriving show]
end

module ThisParam : sig
type ('M, 'T) t = 'M * ('M, 'T) t'

and ('M, 'T) t' = {
annot: ('M, 'T) Type.t;
comments: ('M, unit) Syntax.t option;
}
[@@deriving show]
end

module Params : sig
type ('M, 'T) t = 'M * ('M, 'T) t'

and ('M, 'T) t' = {
this_: ('M, 'T) ThisParam.t option;
params: ('M, 'T) Param.t list;
rest: ('M, 'T) RestParam.t option;
comments: ('M, 'M Comment.t list) Syntax.t option;
Expand Down Expand Up @@ -1800,6 +1811,7 @@ and Function : sig
type ('M, 'T) t = 'M * ('M, 'T) t'

and ('M, 'T) t' = {
this_: ('M * ('M, 'T) Type.annotation) option;
params: ('M, 'T) Param.t list;
rest: ('M, 'T) RestParam.t option;
comments: ('M, 'M Comment.t list) Syntax.t option;
Expand Down
34 changes: 29 additions & 5 deletions src/parser/flow_ast_mapper.ml
Expand Up @@ -994,16 +994,27 @@ class ['loc] mapper =
else
(loc, { argument = argument'; comments = comments' })

method function_this_param_type (this_param : ('loc, 'loc) Ast.Type.Function.ThisParam.t) =
let open Ast.Type.Function.ThisParam in
let (loc, { annot; comments }) = this_param in
let annot' = this#type_ annot in
let comments' = this#syntax_opt comments in
if annot' == annot && comments' == comments then
this_param
else
(loc, { annot = annot'; comments = comments' })

method function_type _loc (ft : ('loc, 'loc) Ast.Type.Function.t) =
let open Ast.Type.Function in
let {
params = (params_loc, { Params.params = ps; rest = rpo; comments = params_comments });
params = (params_loc, { Params.this_; params = ps; rest = rpo; comments = params_comments });
return;
tparams;
comments = func_comments;
} =
ft
in
let this_' = map_opt this#function_this_param_type this_ in
let ps' = map_list this#function_param_type ps in
let rpo' = map_opt this#function_rest_param_type rpo in
let return' = this#type_ return in
Expand All @@ -1017,11 +1028,14 @@ class ['loc] mapper =
&& tparams' == tparams
&& func_comments' == func_comments
&& params_comments' == params_comments
&& this_' == this_
then
ft
else
{
params = (params_loc, { Params.params = ps'; rest = rpo'; comments = params_comments' });
params =
( params_loc,
{ Params.this_ = this_'; params = ps'; rest = rpo'; comments = params_comments' } );
return = return';
tparams = tparams';
comments = func_comments';
Expand Down Expand Up @@ -1410,14 +1424,24 @@ class ['loc] mapper =

method function_params (params : ('loc, 'loc) Ast.Function.Params.t) =
let open Ast.Function in
let (loc, { Params.params = params_list; rest; comments }) = params in
let (loc, { Params.params = params_list; rest; comments; this_ }) = params in
let params_list' = map_list this#function_param params_list in
let rest' = map_opt this#function_rest_param rest in
let this_' = map_opt this#function_this_param this_ in
let comments' = this#syntax_opt comments in
if params_list == params_list' && rest == rest' && comments == comments' then
if params_list == params_list' && rest == rest' && comments == comments' && this_ == this_'
then
params
else
(loc, { Params.params = params_list'; rest = rest'; comments = comments' })
(loc, { Params.params = params_list'; rest = rest'; comments = comments'; this_ = this_' })

method function_this_param (this_param : 'loc * ('loc, 'loc) Ast.Type.annotation) =
let (loc, annotation) = this_param in
let annotation' = this#type_annotation annotation in
if annotation == annotation' then
this_param
else
(loc, annotation')

method function_param (param : ('loc, 'loc) Ast.Function.Param.t) =
let open Ast.Function.Param in
Expand Down

0 comments on commit 8efdc8c

Please sign in to comment.