diff --git a/src/common/errors/error_codes.ml b/src/common/errors/error_codes.ml index 03fbb0df067..06b21624afa 100644 --- a/src/common/errors/error_codes.ml +++ b/src/common/errors/error_codes.ml @@ -107,6 +107,7 @@ type error_code = | InvalidTempType | LintSetting | MalformedPackage + | MatchInvalidPattern | MatchNotExhaustive | MethodUnbinding | MissingLocalAnnot @@ -320,6 +321,7 @@ let string_of_code : error_code -> string = function | InvalidTempType -> "invalid-temp-type" | LintSetting -> "lint-setting" | MalformedPackage -> "malformed-package" + | MatchInvalidPattern -> "match-invalid-pattern" | MatchNotExhaustive -> "match-not-exhaustive" | MethodUnbinding -> "method-unbinding" | MissingLocalAnnot -> "missing-local-annot" diff --git a/src/typing/debug_js.ml b/src/typing/debug_js.ml index 8468d74e179..82054e8322b 100644 --- a/src/typing/debug_js.ml +++ b/src/typing/debug_js.ml @@ -1900,6 +1900,11 @@ let dump_error_message = spf "ENegativeTypeGuardConsistency (%s)" (dump_reason cx reason) | EMatchNotExhaustive { loc; reason } -> spf "EMatchNotExhaustive (%s) (%s)" (string_of_aloc loc) (dump_reason cx reason) + | EMatchInvalidBindingKind { loc; kind } -> + spf + "EMatchInvalidBindingKind (%s) (%s)" + (string_of_aloc loc) + (Flow_ast_utils.string_of_variable_kind kind) | EDevOnlyRefinedLocInfo { refined_loc; refining_locs = _ } -> spf "EDevOnlyRefinedLocInfo {refined_loc=%s}" (string_of_aloc refined_loc) | EDevOnlyInvalidatedRefinementInfo { read_loc; invalidation_info = _ } -> diff --git a/src/typing/errors/error_message.ml b/src/typing/errors/error_message.ml index 893ce0ec7ec..c91635b5774 100644 --- a/src/typing/errors/error_message.ml +++ b/src/typing/errors/error_message.ml @@ -618,6 +618,10 @@ and 'loc t' = loc: 'loc; reason: 'loc virtual_reason; } + | EMatchInvalidBindingKind of { + loc: 'loc; + kind: Flow_ast.Variable.kind; + } (* Dev only *) | EDevOnlyRefinedLocInfo of { refined_loc: 'loc; @@ -1421,6 +1425,7 @@ let rec map_loc_of_error_message (f : 'a -> 'b) : 'a t' -> 'b t' = EDevOnlyRefinedLocInfo { refined_loc = f refined_loc; refining_locs = List.map f refining_locs } | EMatchNotExhaustive { loc; reason } -> EMatchNotExhaustive { loc = f loc; reason = map_reason reason } + | EMatchInvalidBindingKind { loc; kind } -> EMatchInvalidBindingKind { loc = f loc; kind } | EDevOnlyInvalidatedRefinementInfo { read_loc; invalidation_info } -> EDevOnlyInvalidatedRefinementInfo { @@ -1718,7 +1723,8 @@ let util_use_op_of_msg nope util = function | EUnionOptimization _ | EUnionOptimizationOnNonUnion _ | ECannotCallReactComponent _ - | EMatchNotExhaustive _ -> + | EMatchNotExhaustive _ + | EMatchInvalidBindingKind _ -> nope (* Not all messages (i.e. those whose locations are based on use_ops) have locations that can be @@ -1920,6 +1926,7 @@ let loc_of_msg : 'loc t' -> 'loc option = function | EEmptyArrayNoProvider { loc } -> Some loc | EUnusedPromise { loc; _ } -> Some loc | EMatchNotExhaustive { loc; _ } -> Some loc + | EMatchInvalidBindingKind { loc; _ } -> Some loc | EDevOnlyRefinedLocInfo { refined_loc; refining_locs = _ } -> Some refined_loc | EDevOnlyInvalidatedRefinementInfo { read_loc; invalidation_info = _ } -> Some read_loc | EUnableToSpread _ @@ -2868,6 +2875,7 @@ let friendly_message_of_msg = function Normal (MessageInvalidUseOfFlowEnforceOptimized arg) | ECannotCallReactComponent { reason } -> Normal (MessageCannotCallReactComponent reason) | EMatchNotExhaustive { loc = _; reason } -> Normal (MessageMatchNotExhaustive reason) + | EMatchInvalidBindingKind { loc = _; kind } -> Normal (MessageMatchInvalidBindingKind { kind }) let defered_in_speculation = function | EUntypedTypeImport _ @@ -3205,3 +3213,4 @@ let error_code_of_message err : error_code option = | EUnionOptimizationOnNonUnion _ -> Some UnionUnoptimizable | ECannotCallReactComponent _ -> Some ReactRuleCallComponent | EMatchNotExhaustive _ -> Some MatchNotExhaustive + | EMatchInvalidBindingKind _ -> Some MatchInvalidPattern diff --git a/src/typing/errors/flow_intermediate_error.ml b/src/typing/errors/flow_intermediate_error.ml index 48e8a635ebf..0862c98a095 100644 --- a/src/typing/errors/flow_intermediate_error.ml +++ b/src/typing/errors/flow_intermediate_error.ml @@ -3989,6 +3989,14 @@ let to_printable_error : ref reason; text " has not been fully checked against by the match patterns below."; ] + | MessageMatchInvalidBindingKind { kind } -> + [ + text "Cannot use "; + code (Flow_ast_utils.string_of_variable_kind kind); + text " for match pattern binding. Only "; + code "const"; + text " is allowed."; + ] in let rec convert_error_message { kind; loc; error_code; root; message; misplaced_source_file = _ } = diff --git a/src/typing/errors/flow_intermediate_error_types.ml b/src/typing/errors/flow_intermediate_error_types.ml index 8913bea2b1b..18e5e86935b 100644 --- a/src/typing/errors/flow_intermediate_error_types.ml +++ b/src/typing/errors/flow_intermediate_error_types.ml @@ -898,6 +898,7 @@ type 'loc message = null_loc: 'loc option; } | MessageMatchNotExhaustive of 'loc virtual_reason + | MessageMatchInvalidBindingKind of { kind: Flow_ast.Variable.kind } type 'loc intermediate_error = { kind: Flow_errors_utils.error_kind; diff --git a/src/typing/match_pattern.ml b/src/typing/match_pattern.ml index b7807025b15..1c68aa23a01 100644 --- a/src/typing/match_pattern.ml +++ b/src/typing/match_pattern.ml @@ -6,6 +6,7 @@ *) module Ast = Flow_ast +module Tast_utils = Typed_ast_utils open Reason open Type @@ -67,10 +68,17 @@ let binding_identifier cx ~on_binding ~kind acc (loc, { Ast.Identifier.name; com let t = binding cx ~on_binding ~kind acc loc name in ((loc, t), { Ast.Identifier.name; comments }) -let binding_pattern cx ~on_binding acc binding = +let binding_pattern cx ~on_binding ~loc acc binding = let open Ast.MatchPattern.BindingPattern in let { kind; id; comments } = binding in - let id = binding_identifier cx ~on_binding ~kind acc id in + let id = + match kind with + | Ast.Variable.Var + | Ast.Variable.Let -> + Flow_js.add_output cx (Error_message.EMatchInvalidBindingKind { loc; kind }); + Tast_utils.error_mapper#t_identifier id + | Ast.Variable.Const -> binding_identifier cx ~on_binding ~kind acc id + in { kind; id; comments } let rec member cx ~on_identifier ~on_expression mem = @@ -113,7 +121,7 @@ let rest_pattern cx ~on_binding acc rest = { argument = Base.Option.map argument ~f:(fun (arg_loc, arg) -> - (arg_loc, binding_pattern cx ~on_binding acc arg) + (arg_loc, binding_pattern cx ~on_binding ~loc:arg_loc acc arg) ); comments; } @@ -144,7 +152,7 @@ let rec pattern cx ~on_identifier ~on_expression ~on_binding acc (loc, p) : let target = match target with | AsPattern.Binding (loc, binding) -> - AsPattern.Binding (loc, binding_pattern cx ~on_binding acc binding) + AsPattern.Binding (loc, binding_pattern cx ~on_binding ~loc acc binding) | AsPattern.Identifier id -> AsPattern.Identifier (binding_identifier cx ~on_binding ~kind:Ast.Variable.Const acc id) in @@ -152,7 +160,7 @@ let rec pattern cx ~on_identifier ~on_expression ~on_binding acc (loc, p) : | IdentifierPattern (loc, x) -> let t = on_identifier cx x loc in IdentifierPattern ((loc, t), x) - | BindingPattern x -> BindingPattern (binding_pattern cx ~on_binding acc x) + | BindingPattern x -> BindingPattern (binding_pattern cx ~on_binding ~loc acc x) | WildcardPattern x -> WildcardPattern x | ArrayPattern { ArrayPattern.elements; rest; comments } -> let rest = rest_pattern cx ~on_binding acc rest in diff --git a/tests/match/match.exp b/tests/match/match.exp index b4d319efbe0..4b9937074b5 100644 --- a/tests/match/match.exp +++ b/tests/match/match.exp @@ -397,6 +397,86 @@ References: ^^^^^^^^^^^ [1] +Error ------------------------------------------------------------------------------------------- pattern-errors.js:6:10 + +Cannot use `let` for match pattern binding. Only `const` is allowed. [match-invalid-pattern] + + 6| [...let a]: 0, // ERROR + ^^^^^ + + +Error -------------------------------------------------------------------------------------------- pattern-errors.js:7:7 + +Cannot use `let` for match pattern binding. Only `const` is allowed. [match-invalid-pattern] + + 7| {let a, ...let b}: 0, // ERROR + ^^^^^ + + +Error ------------------------------------------------------------------------------------------- pattern-errors.js:7:17 + +Cannot use `let` for match pattern binding. Only `const` is allowed. [match-invalid-pattern] + + 7| {let a, ...let b}: 0, // ERROR + ^^^^^ + + +Error ------------------------------------------------------------------------------------------- pattern-errors.js:8:11 + +Cannot use `let` for match pattern binding. Only `const` is allowed. [match-invalid-pattern] + + 8| 0 as let a: 0, // ERROR + ^^^^^ + + +Error -------------------------------------------------------------------------------------------- pattern-errors.js:9:6 + +Cannot use `let` for match pattern binding. Only `const` is allowed. [match-invalid-pattern] + + 9| let a: 0, // ERROR + ^^^^^ + + +Error ------------------------------------------------------------------------------------------ pattern-errors.js:13:10 + +Cannot use `var` for match pattern binding. Only `const` is allowed. [match-invalid-pattern] + + 13| [...var a]: 0, // ERROR + ^^^^^ + + +Error ------------------------------------------------------------------------------------------- pattern-errors.js:14:7 + +Cannot use `var` for match pattern binding. Only `const` is allowed. [match-invalid-pattern] + + 14| {var a, ...var b}: 0, // ERROR + ^^^^^ + + +Error ------------------------------------------------------------------------------------------ pattern-errors.js:14:17 + +Cannot use `var` for match pattern binding. Only `const` is allowed. [match-invalid-pattern] + + 14| {var a, ...var b}: 0, // ERROR + ^^^^^ + + +Error ------------------------------------------------------------------------------------------ pattern-errors.js:15:11 + +Cannot use `var` for match pattern binding. Only `const` is allowed. [match-invalid-pattern] + + 15| 0 as var a: 0, // ERROR + ^^^^^ + + +Error ------------------------------------------------------------------------------------------- pattern-errors.js:16:6 + +Cannot use `var` for match pattern binding. Only `const` is allowed. [match-invalid-pattern] + + 16| var a: 0, // ERROR + ^^^^^ + + Error -------------------------------------------------------------------------------------------------- patterns.js:9:3 Cannot cast `out` to empty because number [1] is incompatible with empty [2]. [incompatible-cast] @@ -416,127 +496,110 @@ References: Error ------------------------------------------------------------------------------------------------- patterns.js:20:3 -Cannot cast `out` to empty because number [1] is incompatible with empty [2]. [incompatible-cast] - - patterns.js:20:3 - 20| out as empty; // ERROR - ^^^ - -References: - patterns.js:14:20 - 14| declare const x: number; - ^^^^^^ [1] - patterns.js:20:10 - 20| out as empty; // ERROR - ^^^^^ [2] - - -Error ------------------------------------------------------------------------------------------------- patterns.js:31:3 - Cannot resolve name `a`. [cannot-resolve-name] - 31| a; // ERROR: cannot resolve name + 20| a; // ERROR: cannot resolve name ^ -Error ------------------------------------------------------------------------------------------------- patterns.js:42:3 +Error ------------------------------------------------------------------------------------------------- patterns.js:31:3 Cannot cast `out` to empty because number [1] is incompatible with empty [2]. [incompatible-cast] - patterns.js:42:3 - 42| out as empty; // ERROR + patterns.js:31:3 + 31| out as empty; // ERROR ^^^ References: - patterns.js:36:21 - 36| declare const x: [number]; + patterns.js:25:21 + 25| declare const x: [number]; ^^^^^^ [1] - patterns.js:42:10 - 42| out as empty; // ERROR + patterns.js:31:10 + 31| out as empty; // ERROR ^^^^^ [2] -Error ------------------------------------------------------------------------------------------------- patterns.js:54:3 +Error ------------------------------------------------------------------------------------------------- patterns.js:43:3 Cannot cast `out` to empty because number [1] is incompatible with empty [2]. [incompatible-cast] - patterns.js:54:3 - 54| out as empty; // ERROR + patterns.js:43:3 + 43| out as empty; // ERROR ^^^ References: - patterns.js:47:26 - 47| declare const x: {foo: number}; + patterns.js:36:26 + 36| declare const x: {foo: number}; ^^^^^^ [1] - patterns.js:54:10 - 54| out as empty; // ERROR + patterns.js:43:10 + 43| out as empty; // ERROR ^^^^^ [2] -Error ------------------------------------------------------------------------------------------------- patterns.js:66:3 +Error ------------------------------------------------------------------------------------------------- patterns.js:55:3 Cannot cast `out` to empty because number [1] is incompatible with empty [2]. [incompatible-cast] - patterns.js:66:3 - 66| out as empty; // ERROR + patterns.js:55:3 + 55| out as empty; // ERROR ^^^ References: - patterns.js:59:26 - 59| declare const x: {foo: number}; + patterns.js:48:26 + 48| declare const x: {foo: number}; ^^^^^^ [1] - patterns.js:66:10 - 66| out as empty; // ERROR + patterns.js:55:10 + 55| out as empty; // ERROR ^^^^^ [2] -Error ------------------------------------------------------------------------------------------------- patterns.js:79:3 +Error ------------------------------------------------------------------------------------------------- patterns.js:68:3 Cannot cast `out` to empty because number [1] is incompatible with empty [2]. [incompatible-cast] - patterns.js:79:3 - 79| out as empty; // ERROR + patterns.js:68:3 + 68| out as empty; // ERROR ^^^ References: - patterns.js:71:33 - 71| declare const x: {foo: [{bar: number}]}; + patterns.js:60:33 + 60| declare const x: {foo: [{bar: number}]}; ^^^^^^ [1] - patterns.js:79:10 - 79| out as empty; // ERROR + patterns.js:68:10 + 68| out as empty; // ERROR ^^^^^ [2] -Error ------------------------------------------------------------------------------------------------- patterns.js:87:6 +Error ------------------------------------------------------------------------------------------------- patterns.js:76:6 Cannot use variable `a` [1] because the declaration either comes later or was skipped. [reference-before-declaration] - patterns.js:87:6 - 87| [a, const a]: a, // ERROR: reference before declaration + patterns.js:76:6 + 76| [a, const a]: a, // ERROR: reference before declaration ^ References: - patterns.js:87:15 - 87| [a, const a]: a, // ERROR: reference before declaration + patterns.js:76:15 + 76| [a, const a]: a, // ERROR: reference before declaration ^ [1] -Error ----------------------------------------------------------------------------------------------- patterns.js:100:23 +Error ------------------------------------------------------------------------------------------------ patterns.js:89:23 Cannot cast `n` to empty because number [1] is incompatible with empty [2]. [incompatible-cast] - patterns.js:100:23 - 100| {foo: const n} if n as empty: n, // ERROR - ^ + patterns.js:89:23 + 89| {foo: const n} if n as empty: n, // ERROR + ^ References: - patterns.js:94:26 - 94| declare const x: {foo: number}; - ^^^^^^ [1] - patterns.js:100:28 - 100| {foo: const n} if n as empty: n, // ERROR - ^^^^^ [2] + patterns.js:83:26 + 83| declare const x: {foo: number}; + ^^^^^^ [1] + patterns.js:89:28 + 89| {foo: const n} if n as empty: n, // ERROR + ^^^^^ [2] -Found 35 errors +Found 44 errors diff --git a/tests/match/pattern-errors.js b/tests/match/pattern-errors.js new file mode 100644 index 00000000000..e36bd5e218c --- /dev/null +++ b/tests/match/pattern-errors.js @@ -0,0 +1,18 @@ +// Match expressions only allow `const` bindings +{ + declare const x: 0 | 1 | [2] | {a: 3, b: 4}; + + const e1 = match (x) { + [...let a]: 0, // ERROR + {let a, ...let b}: 0, // ERROR + 0 as let a: 0, // ERROR + let a: 0, // ERROR + }; + + const e2 = match (x) { + [...var a]: 0, // ERROR + {var a, ...var b}: 0, // ERROR + 0 as var a: 0, // ERROR + var a: 0, // ERROR + }; +} diff --git a/tests/match/patterns.js b/tests/match/patterns.js index 2d52763ae12..51a6caffa62 100644 --- a/tests/match/patterns.js +++ b/tests/match/patterns.js @@ -9,17 +9,6 @@ out as empty; // ERROR } -// Top level binding with `let` -{ - declare const x: number; - - const out = match (x) { - let a: a, - }; - out as number; // OK - out as empty; // ERROR -} - // Binding doesn't leak to outer scope { declare const x: number;