diff --git a/changelog_unreleased/flow/17681.md b/changelog_unreleased/flow/17681.md new file mode 100644 index 000000000..998143e73 --- /dev/null +++ b/changelog_unreleased/flow/17681.md @@ -0,0 +1,26 @@ +#### Add basic support for "match" syntax (#17681 by @fisker) + + +```jsx +// Input +const e = match (a) { + 1 => true, + 'foo' => false, + 2 => {obj: 'literal'}, +}; + +// Prettier stable +SyntaxError: Unexpected token `{`, expected the token `;` (1:21) +> 1 | const e = match (a) { + | ^ + 2 | 1 => true, + 3 | 'foo' => false, + 4 | 2 => {obj: 'literal'}, + +// Prettier main +const e = match (a) { + 1 => true, + "foo" => false, + 2 => { obj: "literal" }, +}; +``` diff --git a/src/language-js/comments/handle-comments.js b/src/language-js/comments/handle-comments.js index 84ca9f1b9..4c3cb4e99 100644 --- a/src/language-js/comments/handle-comments.js +++ b/src/language-js/comments/handle-comments.js @@ -61,6 +61,7 @@ function handleOwnLineComment(context) { handleClassComments, handleForComments, handleUnionTypeComments, + handleMatchOrPatternComments, handleOnlyComments, handleModuleSpecifiersComments, handleAssignmentPatternComments, @@ -770,6 +771,36 @@ function handleUnionTypeComments({ return false; } +function handleMatchOrPatternComments({ + comment, + precedingNode, + enclosingNode, + followingNode, +}) { + if (enclosingNode && enclosingNode.type === "MatchOrPattern") { + if (isPrettierIgnoreComment(comment)) { + followingNode.prettierIgnore = true; + comment.unignore = true; + } + if (precedingNode) { + addTrailingComment(precedingNode, comment); + return true; + } + return false; + } + + if ( + followingNode && + followingNode.type === "MatchOrPattern" && + isPrettierIgnoreComment(comment) + ) { + followingNode.types[0].prettierIgnore = true; + comment.unignore = true; + } + + return false; +} + function handlePropertyComments({ comment, enclosingNode }) { if (isObjectProperty(enclosingNode)) { addLeadingComment(enclosingNode, comment); diff --git a/src/language-js/comments/printer-methods.js b/src/language-js/comments/printer-methods.js index 5d43ccea1..15bbe61a6 100644 --- a/src/language-js/comments/printer-methods.js +++ b/src/language-js/comments/printer-methods.js @@ -74,6 +74,7 @@ function willPrintOwnComments(path) { (parent.type === "JSXSpreadAttribute" || parent.type === "JSXSpreadChild" || isUnionType(parent) || + parent.type === "MatchOrPattern" || ((parent.type === "ClassDeclaration" || parent.type === "ClassExpression") && parent.superClass === node)))) && diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js index 34a208047..b6aca54a2 100644 --- a/src/language-js/needs-parens.js +++ b/src/language-js/needs-parens.js @@ -621,6 +621,7 @@ function needsParens(path, options) { case "UnionTypeAnnotation": return ( parent.type === "TypeOperator" || + parent.type === "KeyofTypeAnnotation" || parent.type === "ArrayTypeAnnotation" || parent.type === "NullableTypeAnnotation" || parent.type === "IntersectionTypeAnnotation" || @@ -877,6 +878,7 @@ function needsParens(path, options) { case "LogicalExpression": case "AwaitExpression": case "TSTypeAssertion": + case "MatchExpressionCase": return true; case "ConditionalExpression": @@ -985,11 +987,15 @@ function needsParens(path, options) { parent.type !== "ThrowStatement" && parent.type !== "TypeCastExpression" && parent.type !== "VariableDeclarator" && - parent.type !== "YieldExpression") + parent.type !== "YieldExpression" && + parent.type !== "MatchExpressionCase") ); case "TSInstantiationExpression": return key === "object" && isMemberExpression(parent); + + case "MatchOrPattern": + return parent.type === "MatchAsPattern"; } return false; diff --git a/src/language-js/parse/flow.js b/src/language-js/parse/flow.js index aa1d37a02..578e4402f 100644 --- a/src/language-js/parse/flow.js +++ b/src/language-js/parse/flow.js @@ -18,6 +18,8 @@ const parseOptions = { esproposal_decorators: true, // `esproposal_export_star_as` (boolean, default `false`) - enable parsing of `export * as` syntax esproposal_export_star_as: true, + // Undocumented + pattern_matching: true, // `types` (boolean, default `true`) - enable parsing of Flow types // types: true, // `use_strict` (boolean, default `false`) - treat the file as strict, without needing a "use strict" directive diff --git a/src/language-js/parse/hermes.js b/src/language-js/parse/hermes.js index dd32db3a0..5416e0eb0 100644 --- a/src/language-js/parse/hermes.js +++ b/src/language-js/parse/hermes.js @@ -31,8 +31,8 @@ function createParseError(error) { const parseOptions = { flow: "all", babel: false, - // Not supported yet - enableExperimentalFlowMatchSyntax: false, + // enableExperimentalComponentSyntax: true, // Enable by default + // enableExperimentalFlowMatchSyntax: true, // Enable by default tokens: false, allowReturnOutsideFunction: true, }; diff --git a/src/language-js/print/array.js b/src/language-js/print/array.js index 4d9f8e0c5..bd91191ae 100644 --- a/src/language-js/print/array.js +++ b/src/language-js/print/array.js @@ -175,15 +175,24 @@ function isConciselyPrintedArray(node, options) { } function isLineAfterElementEmpty({ node }, { originalText: text }) { - const skipComment = (idx) => - skipInlineComment(text, skipTrailingComment(text, idx)); + let currentIdx = locEnd(node); + if (currentIdx === locStart(node)) { + return false; + } - const skipToComma = (currentIdx) => - text[currentIdx] === "," - ? currentIdx - : skipToComma(skipComment(currentIdx + 1)); + const { length } = text; + while (currentIdx < length) { + if (text[currentIdx] === ",") { + break; + } - return isNextLineEmptyAfterIndex(text, skipToComma(locEnd(node))); + currentIdx = skipInlineComment( + text, + skipTrailingComment(text, currentIdx + 1), + ); + } + + return isNextLineEmptyAfterIndex(text, currentIdx); } function printArrayElements(path, options, print, elementsProperty, inexact) { diff --git a/src/language-js/print/block.js b/src/language-js/print/block.js index 0eef858b7..253d554b6 100644 --- a/src/language-js/print/block.js +++ b/src/language-js/print/block.js @@ -52,6 +52,7 @@ function printBlock(path, options, print) { parent.type === "ModuleExpression" || (parent.type === "CatchClause" && !parentParent.finalizer) || parent.type === "TSModuleDeclaration" || + parent.type === "MatchStatementCase" || node.type === "StaticBlock" ) ) { diff --git a/src/language-js/print/flow.js b/src/language-js/print/flow.js index 3971a2a78..7bbca9c48 100644 --- a/src/language-js/print/flow.js +++ b/src/language-js/print/flow.js @@ -28,6 +28,7 @@ import { import { printInterface } from "./interface.js"; import { printBigInt } from "./literal.js"; import { printFlowMappedTypeProperty } from "./mapped-type.js"; +import { printMatch, printMatchCase, printMatchPattern } from "./match.js"; import { printDeclareToken, printOptionalToken, @@ -323,6 +324,26 @@ function printFlow(path, options, print) { case "AsConstExpression": case "SatisfiesExpression": return printBinaryCastExpression(path, options, print); + + case "MatchExpression": + case "MatchStatement": + return printMatch(path, options, print); + case "MatchExpressionCase": + case "MatchStatementCase": + return printMatchCase(path, options, print); + case "MatchOrPattern": + case "MatchAsPattern": + case "MatchWildcardPattern": + case "MatchLiteralPattern": + case "MatchUnaryPattern": + case "MatchIdentifierPattern": + case "MatchMemberPattern": + case "MatchBindingPattern": + case "MatchObjectPattern": + case "MatchObjectPatternProperty": + case "MatchRestPattern": + case "MatchArrayPattern": + return printMatchPattern(path, options, print); } } diff --git a/src/language-js/print/jsx.js b/src/language-js/print/jsx.js index 20b106a7c..b1281959e 100644 --- a/src/language-js/print/jsx.js +++ b/src/language-js/print/jsx.js @@ -496,6 +496,7 @@ const NO_WRAP_PARENTS = new Set([ "OptionalCallExpression", "ConditionalExpression", "JsExpressionRoot", + "MatchExpressionCase", ]); function maybeWrapJsxElementInParens(path, elem, options) { const { parent } = path; diff --git a/src/language-js/print/match.js b/src/language-js/print/match.js new file mode 100644 index 000000000..8a06a1851 --- /dev/null +++ b/src/language-js/print/match.js @@ -0,0 +1,249 @@ +import { + align, + group, + hardline, + ifBreak, + indent, + join, + line, + softline, +} from "../../document/builders.js"; +import { + printComments, + printDanglingComments, +} from "../../main/comments/print.js"; +import pathNeedsParens from "../needs-parens.js"; +import { + CommentCheckFlags, + createTypeCheckFunction, + hasComment, + hasLeadingOwnLineComment, + isNextLineEmpty, +} from "../utils/index.js"; + +/* +- `MatchExpression` (Flow) +- `MatchStatement` (Flow) +*/ +export function printMatch(path, options, print) { + const { node } = path; + return [ + group(["match (", indent([softline, print("argument")]), softline, ")"]), + " {", + node.cases.length > 0 + ? indent([ + hardline, + join( + hardline, + path.map( + ({ node, isLast }) => [ + print(), + !isLast && isNextLineEmpty(node, options) ? hardline : "", + ], + "cases", + ), + ), + ]) + : "", + hardline, + "}", + ]; +} + +/* +- `MatchExpressionCase` (Flow) +- `MatchStatementCase` (Flow) +*/ +export function printMatchCase(path, options, print) { + const { node } = path; + + const comment = hasComment(node, CommentCheckFlags.Dangling) + ? [" ", printDanglingComments(path, options)] + : []; + + const body = + node.type === "MatchStatementCase" + ? [" ", print("body")] + : indent([line, print("body"), ","]); + + return [ + print("pattern"), + node.guard ? group([indent([line, "if (", print("guard"), ")"])]) : "", + group([" =>", comment, body]), + ]; +} + +/* +- `MatchOrPattern` (Flow) +- `MatchAsPattern` (Flow) +- `MatchWildcardPattern` (Flow) +- `MatchLiteralPattern` (Flow) +- `MatchUnaryPattern` (Flow) +- `MatchIdentifierPattern` (Flow) +- `MatchMemberPattern` (Flow) +- `MatchBindingPattern` (Flow) +- `MatchObjectPattern` (Flow) +- `MatchArrayPattern` (Flow) +- `MatchObjectPatternProperty` (Flow) +- `MatchRestPattern` (Flow) +*/ +export function printMatchPattern(path, options, print) { + const { node } = path; + + switch (node.type) { + case "MatchOrPattern": + return printMatchOrPattern(path, options, print); + case "MatchAsPattern": + return [print("pattern"), " as ", print("target")]; + case "MatchWildcardPattern": + return ["_"]; + case "MatchLiteralPattern": + return print("literal"); + case "MatchUnaryPattern": + return [node.operator, print("argument")]; + case "MatchIdentifierPattern": + return print("id"); + case "MatchMemberPattern": { + const property = + node.property.type === "Identifier" + ? [".", print("property")] + : ["[", indent([softline, print("property")]), softline, "]"]; + return group([print("base"), property]); + } + case "MatchBindingPattern": + return [node.kind, " ", print("id")]; + case "MatchObjectPattern": { + const properties = path.map(print, "properties"); + if (node.rest) { + properties.push(print("rest")); + } + return group([ + "{", + indent([softline, join([",", line], properties)]), + node.rest ? "" : ifBreak(","), + softline, + "}", + ]); + } + case "MatchArrayPattern": { + const elements = path.map(print, "elements"); + if (node.rest) { + elements.push(print("rest")); + } + return group([ + "[", + indent([softline, join([",", line], elements)]), + node.rest ? "" : ifBreak(","), + softline, + "]", + ]); + } + case "MatchObjectPatternProperty": + if (node.shorthand) { + return print("pattern"); + } + return group([print("key"), ":", indent([line, print("pattern")])]); + case "MatchRestPattern": { + const parts = ["..."]; + if (node.argument) { + parts.push(print("argument")); + } + return parts; + } + } +} + +const isSimpleMatchPattern = createTypeCheckFunction([ + "MatchWildcardPattern", + "MatchLiteralPattern", + "MatchUnaryPattern", + "MatchIdentifierPattern", +]); + +function shouldHugMatchOrPattern(node) { + const { patterns } = node; + if (patterns.some((node) => hasComment(node))) { + return false; + } + + const objectPattern = patterns.find( + (node) => node.type === "MatchObjectPattern", + ); + if (!objectPattern) { + return false; + } + + return patterns.every( + (node) => node === objectPattern || isSimpleMatchPattern(node), + ); +} + +function shouldHugMatchPattern(node) { + if (isSimpleMatchPattern(node) || node.type === "MatchObjectPattern") { + return true; + } + + if (node.type === "MatchOrPattern") { + return shouldHugMatchOrPattern(node); + } + + return false; +} + +function printMatchOrPattern(path, options, print) { + const { node } = path; + // single-line variation + // A | B | C + + // multi-line variation + // | A + // | B + // | C + + const { parent } = path; + + const shouldIndent = + parent.type !== "MatchStatementCase" && + parent.type !== "MatchExpressionCase" && + parent.type !== "MatchArrayPattern" && + parent.type !== "MatchObjectPatternProperty" && + !hasLeadingOwnLineComment(options.originalText, node); + + // { + // a: foo + // } | null | void + // should be inlined and not be printed in the multi-line variant + const shouldHug = shouldHugMatchPattern(node); + + // We want to align the children but without its comment, so it looks like + // | child1 + // // comment + // | child2 + const printed = path.map((patternPath) => { + let printedPattern = print(); + if (!shouldHug) { + printedPattern = align(2, printedPattern); + } + return printComments(patternPath, printedPattern, options); + }, "patterns"); + + if (shouldHug) { + return join(" | ", printed); + } + + const code = [ifBreak(["| "]), join([line, "| "], printed)]; + + if (pathNeedsParens(path, options)) { + return group([indent([ifBreak([softline]), code]), softline]); + } + + if (parent.type === "MatchArrayPattern" && parent.elements.length > 1) { + return group([ + indent([ifBreak(["(", softline]), code]), + softline, + ifBreak(")"), + ]); + } + + return group(shouldIndent ? indent(code) : code); +} diff --git a/src/language-js/traverse/visitor-keys.evaluate.js b/src/language-js/traverse/visitor-keys.evaluate.js index 224133973..e758fbf62 100644 --- a/src/language-js/traverse/visitor-keys.evaluate.js +++ b/src/language-js/traverse/visitor-keys.evaluate.js @@ -67,24 +67,6 @@ const excludeNodeTypes = new Set([ // Babel, Won't exist since we use `createImportExpressions` when parsing with babel "Import", - // Flow, not supported - "MatchArrayPattern", - "MatchAsPattern", - "MatchBindingPattern", - "MatchExpression", - "MatchExpressionCase", - "MatchIdentifierPattern", - "MatchLiteralPattern", - "MatchMemberPattern", - "MatchObjectPattern", - "MatchObjectPatternProperty", - "MatchOrPattern", - "MatchRestPattern", - "MatchStatement", - "MatchStatementCase", - "MatchUnaryPattern", - "MatchWildcardPattern", - // Babel v7.28.0 // https://github.com/prettier/prettier/pull/17696 "VoidPattern", diff --git a/tests/format/flow-repo/match/__snapshots__/format.test.js.snap b/tests/format/flow-repo/match/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..190cbf9d2 --- /dev/null +++ b/tests/format/flow-repo/match/__snapshots__/format.test.js.snap @@ -0,0 +1,2590 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`expression.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +// Type of body and abnormal handling +{ + declare const x: empty; + const out = match (x) {}; + + out as empty; // OK +} + +declare const x: 1 | 2; + +{ + const out = match (x) { + 1 => true, + 2 => false, + }; + + out as boolean; // OK + out as empty; // ERROR +} + +{ + const out = match (x) { + 1 => true, + 2 => 's', + }; + out as boolean | string; // OK + out as empty; // ERROR +} + +declare function invariant(boolean): empty; + +{ + const out = match (x) { + 1 => true, + 2 => invariant(false), + }; + out as boolean; // OK + out as empty; // ERROR +} + +function f1() { + const out = match (x) { + 1 => invariant(false), + 2 => invariant(false), + }; + out; // ERROR: unreachable +} + +// Throws in guards +function f2() { + const out = match (x) { + 1 if (invariant(false)) => true, + _ => 's', + }; + out as string; // OK + out as empty; // ERROR +} + +// Nested matches +{ + const out = match (x) { + 1 => 1, + const a => match (a) { + const a => a, + }, + }; + + out as number; // OK +} + +// Guards can refine values which are not the argument +{ + declare const y: number | string; + + const out = match (x) { + 1 if (typeof y === 'number') => y as number, // OK + const a if (a === 1) => a as 1, + _ => 0, + }; +} + +// Case body provider and refinement analysis +{ + let target; + const out = match (x) { + 1 => target = "foo", + 2 => target = true, + }; + + target as string | boolean; // OK +} +{ + let target; + const out = match (x) { + const a => target = "foo" as const, + }; + a; // ERROR + + target as "foo"; // OK +} +{ + declare const x: [number] | number; + let target = null; + const out = match (x) { + [const a] => target = "foo", + const a => target = true, + }; + a; // ERROR + + target as string | boolean; // OK +} +{ + let target; + const out = match (x) { + 1 => target = "foo", + 2 => invariant(false), + }; + + target as string; // OK +} +{ + declare const o: {prop: number}; + const out = match (x) { + 1 => o.prop = 1, + 2 => o.prop = 2, + }; + + o.prop as 1 | 2; // OK +} +{ + const a = []; + const out = match (x) { + 1 => a.push(1), + 2 => a.push(2), + }; + + a as Array; // OK +} + +// Natural inference hint +{ + const out: 'a' | 'b' = match (x) { + 1 => 'b', // OK + 2 => 'xxx', // ERROR: 'xxx' is not a subtype of 'a' | 'b' + }; +} +{ + declare const dict: {['a' | 'b']: number}; + dict[match (x) { + 1 => 'b', // OK + 2 => 'xxx', // ERROR: 'xxx' is not a subtype of 'a' | 'b' + }]; +} + +=====================================output===================================== +// Type of body and abnormal handling +{ + declare const x: empty; + const out = match (x) { + }; + + out as empty; // OK +} + +declare const x: 1 | 2; + +{ + const out = match (x) { + 1 => true, + 2 => false, + }; + + out as boolean; // OK + out as empty; // ERROR +} + +{ + const out = match (x) { + 1 => true, + 2 => "s", + }; + out as boolean | string; // OK + out as empty; // ERROR +} + +declare function invariant(boolean): empty; + +{ + const out = match (x) { + 1 => true, + 2 => invariant(false), + }; + out as boolean; // OK + out as empty; // ERROR +} + +function f1() { + const out = match (x) { + 1 => invariant(false), + 2 => invariant(false), + }; + out; // ERROR: unreachable +} + +// Throws in guards +function f2() { + const out = match (x) { + 1 if (invariant(false)) => true, + _ => "s", + }; + out as string; // OK + out as empty; // ERROR +} + +// Nested matches +{ + const out = match (x) { + 1 => 1, + const a => + match (a) { + const a => a, + }, + }; + + out as number; // OK +} + +// Guards can refine values which are not the argument +{ + declare const y: number | string; + + const out = match (x) { + 1 if (typeof y === "number") => y as number, // OK + const a if (a === 1) => a as 1, + _ => 0, + }; +} + +// Case body provider and refinement analysis +{ + let target; + const out = match (x) { + 1 => (target = "foo"), + 2 => (target = true), + }; + + target as string | boolean; // OK +} +{ + let target; + const out = match (x) { + const a => (target = "foo" as const), + }; + a; // ERROR + + target as "foo"; // OK +} +{ + declare const x: [number] | number; + let target = null; + const out = match (x) { + [const a] => (target = "foo"), + const a => (target = true), + }; + a; // ERROR + + target as string | boolean; // OK +} +{ + let target; + const out = match (x) { + 1 => (target = "foo"), + 2 => invariant(false), + }; + + target as string; // OK +} +{ + declare const o: { prop: number }; + const out = match (x) { + 1 => (o.prop = 1), + 2 => (o.prop = 2), + }; + + o.prop as 1 | 2; // OK +} +{ + const a = []; + const out = match (x) { + 1 => a.push(1), + 2 => a.push(2), + }; + + a as Array; // OK +} + +// Natural inference hint +{ + const out: "a" | "b" = match (x) { + 1 => "b", // OK + 2 => "xxx", // ERROR: 'xxx' is not a subtype of 'a' | 'b' + }; +} +{ + declare const dict: { ["a" | "b"]: number }; + dict[ + match (x) { + 1 => "b", // OK + 2 => "xxx", // ERROR: 'xxx' is not a subtype of 'a' | 'b' + } + ]; +} + +================================================================================ +`; + +exports[`hints.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +// Annotation hint +type F = string => boolean; +{ + declare const x: 'a'; + + const out: F = match (x) { + 'a' => (y => true), // OK + }; +} + +// Sibling before +{ + declare const x: 'a' | 'b'; + + const out = match (x) { + 'a' => [1], + 'b' => [], // Should be \`Array\` + }; + out as Array; // OK +} + +// Sibling after +{ + declare const x: 'a' | 'b'; + + const out = match (x) { + 'a' => [], // Should be \`Array\` + 'b' => [1], + }; + out as Array; // OK +} + +// Multiple siblings, one valid +{ + declare const x: 'a' | 'b' | 'c' | 'd'; + + const out = match (x) { + 'a' => 1, + 'b' => {}, + 'c' => [1], + 'd' => [], // Should be \`Array\` + }; + out as number | {} | Array; // OK +} + +// Multiple siblings, multiple valid +{ + declare const x: 'a' | 'b' | 'c'; + + const out = match (x) { + 'a' => [true], + 'b' => [1], + 'c' => [], + }; + out as Array | Array; // OK +} +{ + declare const x: 'a' | 'b' | 'c'; + + const out = match (x) { + 'a' => ((x: number) => 1), + 'b' => ((x: string) => true), + 'c' => (x => x as number), // OK + }; +} + +// Cycles avoided +{ + declare const x: 'a' | 'b'; + + const out: (x: number) => void = match (x) { // OK + 'a' => ((x) => {}), + 'b' => ((x) => {}), + }; +} +{ + declare const x: 'a' | 'b'; + + const out: Array = match (x) { // OK + 'a' => [], + 'b' => [], + }; +} + +=====================================output===================================== +// Annotation hint +type F = (string) => boolean; +{ + declare const x: "a"; + + const out: F = match (x) { + "a" => ((y) => true), // OK + }; +} + +// Sibling before +{ + declare const x: "a" | "b"; + + const out = match (x) { + "a" => [1], + "b" => [], // Should be \`Array\` + }; + out as Array; // OK +} + +// Sibling after +{ + declare const x: "a" | "b"; + + const out = match (x) { + "a" => [], // Should be \`Array\` + "b" => [1], + }; + out as Array; // OK +} + +// Multiple siblings, one valid +{ + declare const x: "a" | "b" | "c" | "d"; + + const out = match (x) { + "a" => 1, + "b" => {}, + "c" => [1], + "d" => [], // Should be \`Array\` + }; + out as number | {} | Array; // OK +} + +// Multiple siblings, multiple valid +{ + declare const x: "a" | "b" | "c"; + + const out = match (x) { + "a" => [true], + "b" => [1], + "c" => [], + }; + out as Array | Array; // OK +} +{ + declare const x: "a" | "b" | "c"; + + const out = match (x) { + "a" => ((x: number) => 1), + "b" => ((x: string) => true), + "c" => ((x) => x as number), // OK + }; +} + +// Cycles avoided +{ + declare const x: "a" | "b"; + + const out: (x: number) => void = match ( + x // OK + ) { + "a" => ((x) => {}), + "b" => ((x) => {}), + }; +} +{ + declare const x: "a" | "b"; + + const out: Array = match ( + x // OK + ) { + "a" => [], + "b" => [], + }; +} + +================================================================================ +`; + +exports[`matching.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +// Primitive value union +{ + declare const x: 1 | -2 | 3n | 's' | false | null; + + const e1 = match (x) { + 1 => 0, + -2 => 0, + 3n => 0, + 's' => 0, + false => 0, + null => 0, + }; + + const e2 = match (x) { // ERROR: not all members checked + 1 => 0, + false => 0, + 3n => 0, + null => 0, + }; +} + +// Identifier patterns +{ + declare const x: 1 | 2; + + declare const one: 1; + declare const two: 2; + + const e1 = match (x) { + one => 0, + two => 0, + }; + + const e2 = match (x) { // ERROR: \`2\` not checked + one => 0, + }; +} + +// \`undefined\` +{ + declare const x: 1 | void; + + const e1 = match (x) { + 1 => 0, + undefined => 0, + }; + + const e2 = match (x) { // ERROR: \`undefined\` not checked + 1 => 0, + }; +} + +// Maybe types +{ + declare const x: ?1; + + const e1 = match (x) { + 1 => 0, + undefined => 0, + null => 0, + }; + + const e2 = match (x) { // ERROR: \`null\` and \`undefined\` not checked + 1 => 0, + }; +} + +// Member patterns +{ + declare const x: 1 | 2; + + declare const o: { + one: 1, + two: 2, + }; + + const e1 = match (x) { + o.one => 0, + o.two => 0, + }; + + const e2 = match (x) { // ERROR: \`2\` not checked + o.one => 0, + }; +} + +// \`as\` pattern refines using its pattern +{ + declare const x: 1 | 2; + + const e1 = match (x) { + 1 as a => a as 1, // OK + 2 as a => a as 2, // OK + }; +} +{ + declare const x: {type: 'foo', value: 1} + | {type: 'bar', value: 2}; + + // Nested \`as\` pattern + const e1 = match (x) { + {type: 'foo' as a, const value} => (a as 'foo', value as 1), // OK + {type: 'bar' as a, const value} => (a as 'bar', value as 2), // OK + }; +} + +// Top level binding and wildcard +{ + declare const x: 1 | 2; + + const e1 = match (x) { + 1 => 0, + const a => a as 2, // OK + }; + + const e2 = match (x) { + 1 => 0, + _ => 0, // OK + }; +} + +// Non-ident/member argument still works +{ + declare const f: () => 1 | 2; + + const e1 = match (f()) { + 1 => 0, + 2 => 0, + }; + + const e2 = match (f()) { // ERROR: \`2\` not checked + 1 => 0, + }; +} + +// Or pattern +{ + declare const x: 1 | 2 | 3; + + const e1 = match (x) { + 1 | 2 | 3 => true, + }; + + const e2 = match (x) { // ERROR: \`3\` not checked + 1 | 2 => true, + }; +} + +// Patterns with guard could match or not match +{ + declare const x: 1 | 2; + + declare function f(): boolean; + + const e1 = match (x) { // ERROR: \`2\` not checked + 1 => 0, + 2 if (f()) => 0, + }; + + const e2 = match (x) { + 1 => 0, + 2 if (f()) => 0, + 2 => 0, + }; +} + +// Property exists +{ + declare const x: {foo: void, a: 0} | {bar: void, a: 1}; + + const e1 = match (x) { + {foo: _, const a} => a as 0, // OK + {bar: _, const a} => a as 1, // OK + }; +} +{ + declare const x: + | {foo: void, a: 0} + | {bar: void, a: 1} + | {baz: void, a: 2} + | {zap: void, a: 3}; + + declare const u: void; + declare const o: {u: void}; + + const e1 = match (x) { + {foo: u, const a} => a as 0, // OK + {bar: o.u, const a} => a as 1, // OK + {baz: undefined as v, const a} => a as 2, // OK + {zap: u, const a} => a as 3, // OK + }; +} + +// Disjoint object union +{ + declare const x: {type: 'foo', val: number} + | {type: 'bar', val: string} + | {type: 'baz', val: boolean}; + + const e1 = match (x) { + {type: 'foo', val: const a} => a as number, // OK + {type: 'bar', val: const a} => a as string, // OK + {type: 'baz', val: const a} => a as boolean, // OK + }; + + const e2 = match (x) { // ERROR: \`type: 'baz'\` not checked + {type: 'foo', val: const a} => a as number, // OK + {type: 'bar', val: const a} => a as string, // OK + }; + + // Using idents as pattern + declare const foo: 'foo'; + declare const bar: 'bar'; + declare const baz: 'baz'; + const e3 = match (x) { + {type: foo, val: const a} => a as number, // OK + {type: bar, val: const a} => a as string, // OK + {type: baz, val: const a} => a as boolean, // OK + }; + + // Using members as pattern + declare const o: { + foo: 'foo', + bar: 'bar', + baz: 'baz', + }; + const e4 = match (x) { + {type: o.foo, val: const a} => a as number, // OK + {type: o.bar, val: const a} => a as string, // OK + {type: o.baz, val: const a} => a as boolean, // OK + }; +} + +// Disjoint object union with multiple pivot props +{ + declare const x: {type: 'foo', val: number} + | {type: 'bar', n: 1, val: string} + | {type: 'bar', n: 2, val: boolean}; + + const e1 = match (x) { + {type: 'foo', val: const a} => a as number, // OK + {type: 'bar', val: const a, ...} => a as string | boolean, // OK + }; + + const e2 = match (x) { + {type: 'foo', val: const a} => a as number, // OK + {type: 'bar', n: 1, val: const a} => a as string, // OK + {type: 'bar', n: 2, val: const a} => a as boolean, // OK + }; + + const e3 = match (x) { // ERROR: \`type: 'bar', n: 2\` not checked + {type: 'foo', val: const a} => a as number, // OK + {type: 'bar', n: 1, val: const a} => a as string, // OK + }; +} + +// Combo union of object with sentinel property and primitive value +{ + declare const x: null | {type: 'bar', val: number}; + + const e1 = match (x) { + {type: 'bar', val: const a} => a as number, // OK + null => 0, + }; +} + +// Or pattern: objects +{ + declare const x: {type: 'foo', val: number} + | {type: 'bar', val: string} + | {type: 'baz', val: boolean}; + + const e1 = match (x) { + {type: 'foo', ...} | {type: 'bar', ...} | {type: 'baz', ...} => 0, + }; + + const e2 = match (x) { // ERROR: \`type: 'bar'\` not checked + {type: 'foo', ...} | {type: 'baz', ...} => 0, + }; +} + +// Disjoint tuple union +{ + declare const x: ['foo', number] + | ['bar', string] + | ['baz', boolean]; + + const e1 = match (x) { + ['foo', const a] => a as number, // OK + ['bar', const a] => a as string, // OK + ['baz', const a] => a as boolean, // OK + }; + + const e2 = match (x) { // ERROR: \`'baz'\` element not checked + ['foo', const a] => a as number, // OK + ['bar', const a] => a as string, // OK + }; + + // Using idents as pattern + declare const foo: 'foo'; + declare const bar: 'bar'; + declare const baz: 'baz'; + const e3 = match (x) { + [foo, const a] => a as number, // OK + [bar, const a] => a as string, // OK + [baz, const a] => a as boolean, // OK + }; +} + +// Combo union of tuples with sentinel property and primitive value +{ + declare const x: null | ['bar', number] | ['foo', string]; + + const e1 = match (x) { + ['bar', const a] => a as number, // OK + ['foo', const a] => a as string, // OK + null => 0, + }; +} + +// Tuple length refinements +{ + declare const x: [number] + | [string, string] + | [boolean, boolean, boolean]; + + const e1 = match (x) { + [const a] => a as number, // OK + [const a, _] => a as string, // OK + [const a, _, _] => a as empty, // ERROR: \`boolean\` is not \`empty\` + }; + + const e2 = match (x) { + [...] => 0, // OK: matches all + } + + const e3 = match (x) { + [const a, _, ...] => a as string | boolean, // OK + [const a] => a as number, // OK + } +} +{ + declare const x: [number] | Array; + + const e1 = match (x) { + [] => 0, // OK + [const a] => a as string, // ERROR: \`number\` is not \`string\` + [const a, _] => a as string, // OK + const d => d as Array, // OK: tuple checked, but array could have other lengths + }; + + const e2 = match (x) { + [...] => 0, // OK: matches all + } +} + +// Optional tuple elements +{ + declare const x: [a: 0, b?: 1, c?: 2]; + + const e1 = match (x) { + [_, ...] => 0, + }; + + const e2 = match (x) { // ERROR: does not match all possibilities + [_, _, ...] => 0, + }; + + const e3 = match (x) { // ERROR: does not match all possibilities + [_] => 0, + }; + + const e4 = match (x) { // ERROR: does not match all possibilities + [_, _, _] => 0, + }; +} + +// Inexact tuple types +{ + declare const x: [a: 0, ...]; + + const e1 = match (x) { + [_, ...] => 0, + }; + + const e2 = match (x) { // ERROR: does not match all elements + [_] => 0, + }; +} + +// Exhaustive checking error points to definition +{ + type T = {foo: 1} | {foo: 2}; + declare const x: T; + + const e1 = match (x) { + {foo: 1} => 0, + }; +} + +=====================================output===================================== +// Primitive value union +{ + declare const x: 1 | -2 | 3n | "s" | false | null; + + const e1 = match (x) { + 1 => 0, + -2 => 0, + 3n => 0, + "s" => 0, + false => 0, + null => 0, + }; + + const e2 = match ( + x // ERROR: not all members checked + ) { + 1 => 0, + false => 0, + 3n => 0, + null => 0, + }; +} + +// Identifier patterns +{ + declare const x: 1 | 2; + + declare const one: 1; + declare const two: 2; + + const e1 = match (x) { + one => 0, + two => 0, + }; + + const e2 = match ( + x // ERROR: \`2\` not checked + ) { + one => 0, + }; +} + +// \`undefined\` +{ + declare const x: 1 | void; + + const e1 = match (x) { + 1 => 0, + undefined => 0, + }; + + const e2 = match ( + x // ERROR: \`undefined\` not checked + ) { + 1 => 0, + }; +} + +// Maybe types +{ + declare const x: ?1; + + const e1 = match (x) { + 1 => 0, + undefined => 0, + null => 0, + }; + + const e2 = match ( + x // ERROR: \`null\` and \`undefined\` not checked + ) { + 1 => 0, + }; +} + +// Member patterns +{ + declare const x: 1 | 2; + + declare const o: { + one: 1, + two: 2, + }; + + const e1 = match (x) { + o.one => 0, + o.two => 0, + }; + + const e2 = match ( + x // ERROR: \`2\` not checked + ) { + o.one => 0, + }; +} + +// \`as\` pattern refines using its pattern +{ + declare const x: 1 | 2; + + const e1 = match (x) { + 1 as a => a as 1, // OK + 2 as a => a as 2, // OK + }; +} +{ + declare const x: { type: "foo", value: 1 } | { type: "bar", value: 2 }; + + // Nested \`as\` pattern + const e1 = match (x) { + {type: "foo" as a, const value} => (a as "foo", value as 1), // OK + {type: "bar" as a, const value} => (a as "bar", value as 2), // OK + }; +} + +// Top level binding and wildcard +{ + declare const x: 1 | 2; + + const e1 = match (x) { + 1 => 0, + const a => a as 2, // OK + }; + + const e2 = match (x) { + 1 => 0, + _ => 0, // OK + }; +} + +// Non-ident/member argument still works +{ + declare const f: () => 1 | 2; + + const e1 = match (f()) { + 1 => 0, + 2 => 0, + }; + + const e2 = match ( + f() // ERROR: \`2\` not checked + ) { + 1 => 0, + }; +} + +// Or pattern +{ + declare const x: 1 | 2 | 3; + + const e1 = match (x) { + 1 | 2 | 3 => true, + }; + + const e2 = match ( + x // ERROR: \`3\` not checked + ) { + 1 | 2 => true, + }; +} + +// Patterns with guard could match or not match +{ + declare const x: 1 | 2; + + declare function f(): boolean; + + const e1 = match ( + x // ERROR: \`2\` not checked + ) { + 1 => 0, + 2 if (f()) => 0, + }; + + const e2 = match (x) { + 1 => 0, + 2 if (f()) => 0, + 2 => 0, + }; +} + +// Property exists +{ + declare const x: { foo: void, a: 0 } | { bar: void, a: 1 }; + + const e1 = match (x) { + {foo: _, const a} => a as 0, // OK + {bar: _, const a} => a as 1, // OK + }; +} +{ + declare const x: + | { foo: void, a: 0 } + | { bar: void, a: 1 } + | { baz: void, a: 2 } + | { zap: void, a: 3 }; + + declare const u: void; + declare const o: { u: void }; + + const e1 = match (x) { + {foo: u, const a} => a as 0, // OK + {bar: o.u, const a} => a as 1, // OK + {baz: undefined as v, const a} => a as 2, // OK + {zap: u, const a} => a as 3, // OK + }; +} + +// Disjoint object union +{ + declare const x: + | { type: "foo", val: number } + | { type: "bar", val: string } + | { type: "baz", val: boolean }; + + const e1 = match (x) { + {type: "foo", val: const a} => a as number, // OK + {type: "bar", val: const a} => a as string, // OK + {type: "baz", val: const a} => a as boolean, // OK + }; + + const e2 = match ( + x // ERROR: \`type: 'baz'\` not checked + ) { + {type: "foo", val: const a} => a as number, // OK + {type: "bar", val: const a} => a as string, // OK + }; + + // Using idents as pattern + declare const foo: "foo"; + declare const bar: "bar"; + declare const baz: "baz"; + const e3 = match (x) { + {type: foo, val: const a} => a as number, // OK + {type: bar, val: const a} => a as string, // OK + {type: baz, val: const a} => a as boolean, // OK + }; + + // Using members as pattern + declare const o: { + foo: "foo", + bar: "bar", + baz: "baz", + }; + const e4 = match (x) { + {type: o.foo, val: const a} => a as number, // OK + {type: o.bar, val: const a} => a as string, // OK + {type: o.baz, val: const a} => a as boolean, // OK + }; +} + +// Disjoint object union with multiple pivot props +{ + declare const x: + | { type: "foo", val: number } + | { type: "bar", n: 1, val: string } + | { type: "bar", n: 2, val: boolean }; + + const e1 = match (x) { + {type: "foo", val: const a} => a as number, // OK + {type: "bar", val: const a, ...} => a as string | boolean, // OK + }; + + const e2 = match (x) { + {type: "foo", val: const a} => a as number, // OK + {type: "bar", n: 1, val: const a} => a as string, // OK + {type: "bar", n: 2, val: const a} => a as boolean, // OK + }; + + const e3 = match ( + x // ERROR: \`type: 'bar', n: 2\` not checked + ) { + {type: "foo", val: const a} => a as number, // OK + {type: "bar", n: 1, val: const a} => a as string, // OK + }; +} + +// Combo union of object with sentinel property and primitive value +{ + declare const x: null | { type: "bar", val: number }; + + const e1 = match (x) { + {type: "bar", val: const a} => a as number, // OK + null => 0, + }; +} + +// Or pattern: objects +{ + declare const x: + | { type: "foo", val: number } + | { type: "bar", val: string } + | { type: "baz", val: boolean }; + + const e1 = match (x) { + {type: "foo", ...} | {type: "bar", ...} | {type: "baz", ...} => 0, + }; + + const e2 = match ( + x // ERROR: \`type: 'bar'\` not checked + ) { + {type: "foo", ...} | {type: "baz", ...} => 0, + }; +} + +// Disjoint tuple union +{ + declare const x: ["foo", number] | ["bar", string] | ["baz", boolean]; + + const e1 = match (x) { + ["foo", const a] => a as number, // OK + ["bar", const a] => a as string, // OK + ["baz", const a] => a as boolean, // OK + }; + + const e2 = match ( + x // ERROR: \`'baz'\` element not checked + ) { + ["foo", const a] => a as number, // OK + ["bar", const a] => a as string, // OK + }; + + // Using idents as pattern + declare const foo: "foo"; + declare const bar: "bar"; + declare const baz: "baz"; + const e3 = match (x) { + [foo, const a] => a as number, // OK + [bar, const a] => a as string, // OK + [baz, const a] => a as boolean, // OK + }; +} + +// Combo union of tuples with sentinel property and primitive value +{ + declare const x: null | ["bar", number] | ["foo", string]; + + const e1 = match (x) { + ["bar", const a] => a as number, // OK + ["foo", const a] => a as string, // OK + null => 0, + }; +} + +// Tuple length refinements +{ + declare const x: [number] | [string, string] | [boolean, boolean, boolean]; + + const e1 = match (x) { + [const a] => a as number, // OK + [const a, _] => a as string, // OK + [const a, _, _] => a as empty, // ERROR: \`boolean\` is not \`empty\` + }; + + const e2 = match (x) { + [...] => 0, // OK: matches all + }; + + const e3 = match (x) { + [const a, _, ...] => a as string | boolean, // OK + [const a] => a as number, // OK + }; +} +{ + declare const x: [number] | Array; + + const e1 = match (x) { + [] => 0, // OK + [const a] => a as string, // ERROR: \`number\` is not \`string\` + [const a, _] => a as string, // OK + const d => d as Array, // OK: tuple checked, but array could have other lengths + }; + + const e2 = match (x) { + [...] => 0, // OK: matches all + }; +} + +// Optional tuple elements +{ + declare const x: [a: 0, b?: 1, c?: 2]; + + const e1 = match (x) { + [_, ...] => 0, + }; + + const e2 = match ( + x // ERROR: does not match all possibilities + ) { + [_, _, ...] => 0, + }; + + const e3 = match ( + x // ERROR: does not match all possibilities + ) { + [_] => 0, + }; + + const e4 = match ( + x // ERROR: does not match all possibilities + ) { + [_, _, _] => 0, + }; +} + +// Inexact tuple types +{ + declare const x: [a: 0, ...]; + + const e1 = match (x) { + [_, ...] => 0, + }; + + const e2 = match ( + x // ERROR: does not match all elements + ) { + [_] => 0, + }; +} + +// Exhaustive checking error points to definition +{ + type T = { foo: 1 } | { foo: 2 }; + declare const x: T; + + const e1 = match (x) { + {foo: 1} => 0, + }; +} + +================================================================================ +`; + +exports[`pattern-errors.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +// 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 + }; +} + +// Invalid numeric property +{ + declare const x: {1: true}; + + const e1 = match (x) { + {1.1: _} => 0, // ERROR + _ => 0, + }; +} + +// Unary pattern on \`0\` banned +{ + declare const x: 0; + + const e1 = match (x) { + -0 => true, // ERROR + +0 => true, // ERROR + 0 => true, // OK + }; +} + +// Unary \`+\` on bigint banned +{ + declare const x: 1n; + + const e1 = match (x) { + +1n => true, // ERROR + 1n => true, // OK + }; +} + +// Duplicate object keys banned +{ + declare const x: {foo: boolean}; + + const e1 = match (x) { + {foo: true, foo: false} => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + {foo: true, const foo} => 0, // ERROR + _ => 0, + }; + + const e3 = match (x) { + {foo: true, 'foo': false} => 0, // ERROR + _ => 0, + }; +} + +// Duplicate binding names +{ + declare const x: [boolean, boolean]; + + const e1 = match (x) { + [const a, true as a] => 0, // ERROR + [const a, const a] => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + [const a, true as const a] => 0, // ERROR + [const a, ...const a] => 0, // ERROR + _ => 0, + }; +} +{ + declare const x: {a: boolean, b: boolean}; + const e1 = match (x) { + {const a, b: true as a} => 0, // ERROR + {const a, ...const a} => 0, // ERROR + _ => 0, + }; +} + +// Bindings in 'or' patterns are not yet supported +{ + declare const x: [boolean] | [string]; + + const e1 = match (x) { + [true as a] | ['s'] => 0, // ERROR + [false as const a] | ['s'] => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + [true as const a] | ['s'] => 0, // ERROR + ['t'] | [...const a] => 0, // ERROR + _ => 0, + }; +} +{ + declare const x: {a: boolean} | {a: string}; + + const e1 = match (x) { + {a: true as a} | {a: 's'} => 0, // ERROR + {a: false as const a} | {a: 's'} => 0, // ERROR + _ => 0, + }; +} + +// As pattern on binding pattern +{ + declare const x: [boolean]; + + const e1 = match (x) { + [const a as b] => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + const a as const b => 0, // ERROR + }; +} + +// Reference before declaration +{ + declare const x: [number, number]; + + const out = match (x) { + [a, const a] => a, // ERROR: reference before declaration + _ => 0, + }; +} +{ + declare const x: [number, {foo: number}]; + + const out = match (x) { + [a.foo, const a] => a, // ERROR: reference before declaration + _ => 0, + }; +} + +// Reference binding introduced in same pattern +{ + declare const x: [number, number]; + + const out = match (x) { + [const a, a] => a, // ERROR + _ => 0, + }; +} +{ + declare const x: [{foo: number}, number]; + + const out = match (x) { + [const a, a.foo] => a, // ERROR + _ => 0, + }; +} + +// // Invalid object property shorthand +// { +// declare const x: {foo: 1, bar: boolean}; + +// const out = match (x) { +// {foo} => 0, // ERROR +// {foo, bar: true} => 0, // ERROR +// _ => 0, +// }; +// } + +// BigInt property usage +{ + declare const x: mixed; + + const out = match (x) { + {1n: 1, ...} => 0, // ERROR + _ => 0, + }; +} + +=====================================output===================================== +// 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 + }; +} + +// Invalid numeric property +{ + declare const x: { 1: true }; + + const e1 = match (x) { + {1.1: _} => 0, // ERROR + _ => 0, + }; +} + +// Unary pattern on \`0\` banned +{ + declare const x: 0; + + const e1 = match (x) { + -0 => true, // ERROR + +0 => true, // ERROR + 0 => true, // OK + }; +} + +// Unary \`+\` on bigint banned +{ + declare const x: 1n; + + const e1 = match (x) { + +1n => true, // ERROR + 1n => true, // OK + }; +} + +// Duplicate object keys banned +{ + declare const x: { foo: boolean }; + + const e1 = match (x) { + {foo: true, foo: false} => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + {foo: true, const foo} => 0, // ERROR + _ => 0, + }; + + const e3 = match (x) { + {foo: true, "foo": false} => 0, // ERROR + _ => 0, + }; +} + +// Duplicate binding names +{ + declare const x: [boolean, boolean]; + + const e1 = match (x) { + [const a, true as a] => 0, // ERROR + [const a, const a] => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + [const a, true as const a] => 0, // ERROR + [const a, ...const a] => 0, // ERROR + _ => 0, + }; +} +{ + declare const x: { a: boolean, b: boolean }; + const e1 = match (x) { + {const a, b: true as a} => 0, // ERROR + {const a, ...const a} => 0, // ERROR + _ => 0, + }; +} + +// Bindings in 'or' patterns are not yet supported +{ + declare const x: [boolean] | [string]; + + const e1 = match (x) { + [true as a] | ["s"] => 0, // ERROR + [false as const a] | ["s"] => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + [true as const a] | ["s"] => 0, // ERROR + ["t"] | [...const a] => 0, // ERROR + _ => 0, + }; +} +{ + declare const x: { a: boolean } | { a: string }; + + const e1 = match (x) { + {a: true as a} | {a: "s"} => 0, // ERROR + {a: false as const a} | {a: "s"} => 0, // ERROR + _ => 0, + }; +} + +// As pattern on binding pattern +{ + declare const x: [boolean]; + + const e1 = match (x) { + [const a as b] => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + const a as const b => 0, // ERROR + }; +} + +// Reference before declaration +{ + declare const x: [number, number]; + + const out = match (x) { + [a, const a] => a, // ERROR: reference before declaration + _ => 0, + }; +} +{ + declare const x: [number, { foo: number }]; + + const out = match (x) { + [a.foo, const a] => a, // ERROR: reference before declaration + _ => 0, + }; +} + +// Reference binding introduced in same pattern +{ + declare const x: [number, number]; + + const out = match (x) { + [const a, a] => a, // ERROR + _ => 0, + }; +} +{ + declare const x: [{ foo: number }, number]; + + const out = match (x) { + [const a, a.foo] => a, // ERROR + _ => 0, + }; +} + +// // Invalid object property shorthand +// { +// declare const x: {foo: 1, bar: boolean}; + +// const out = match (x) { +// {foo} => 0, // ERROR +// {foo, bar: true} => 0, // ERROR +// _ => 0, +// }; +// } + +// BigInt property usage +{ + declare const x: mixed; + + const out = match (x) { + {1n: 1, ...} => 0, // ERROR + _ => 0, + }; +} + +================================================================================ +`; + +exports[`patterns.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +// Top level binding with \`const\` +{ + declare const x: number; + + const out = match (x) { + const a => a, + }; + out as number; // OK + out as empty; // ERROR +} + +// Binding doesn't leak to outer scope +{ + declare const x: number; + + const out = match (x) { + const a => a, + }; + + a; // ERROR: cannot resolve name +} + +// Array pattern +{ + declare const x: [number]; + + const out = match (x) { + [const a] => a, + }; + out as number; // OK + out as empty; // ERROR +} + +// Object pattern +{ + declare const x: {foo: number}; + + const out = match (x) { + {foo: const a} => a, + }; + + out as number; // OK + out as empty; // ERROR +} + +// Object shorthand pattern +{ + declare const x: {foo: number}; + + const out = match (x) { + {const foo} => foo, + }; + + out as number; // OK + out as empty; // ERROR +} + +// Nested patterns +{ + declare const x: {foo: [{bar: number}]}; + + const out1 = match (x) { + {foo: [{bar: const a}]} => a, + _ => 0, + }; + out1 as number; // OK + out1 as empty; // ERROR + + const out2 = match (x) { + {foo: const a} => a[0].bar, + }; + out2 as number; // OK + out2 as empty; // ERROR +} + +// Guards +{ + declare const x: {foo: number}; + declare const f: number => boolean; + + const out = match (x) { + {foo: const n} if (n === 0) => n, // OK + {foo: const n} if (f(n)) => n, // OK + {foo: const n} if (n as empty) => n, // ERROR + _ => 0, + }; +} + +// As pattern +{ + declare const x: {foo: [number]}; + + const out = match (x) { + {foo: [1] as n} => n as [number], // OK + {foo: [1 as n]} => n as number, // OK + _ => 0, + }; +} +{ + declare const x: {foo: [number]}; + + const out = match (x) { + {foo: [1] as const n} => n as [number], // OK + {foo: [1 as const n]} => n as number, // OK + _ => 0, + }; +} +{ + declare const x: {foo: {bar: number}}; + + const out = match (x) { + {foo: {bar: 1}} => 0, // OK + {foo: {bar: 2} as const a} => 0, // OK + {foo: {bar: 3 as const a}} => 0, // OK + {foo: {bar: const a}} => 0, // OK + _ => 0, + }; +} + +// Array rest +{ + declare const x: [1, 2, 3]; + + const out1 = match (x) { + [1, 2, ...const xs] => xs as [3], // OK + _ => 0, + }; + const out2 = match (x) { + [1, ...const xs] => xs as [2, 3], // OK + _ => 0, + }; + const out3 = match (x) { + [...const xs] => xs as [1, 2, 3], // OK + _ => 0, + }; +} + +// Object rest +{ + declare const x: {foo: 1, bar: 2, baz: 3}; + + const out1 = match (x) { + {foo: _, bar: _, ...const xs} => xs as {baz: 3}, // OK + }; + const out2 = match (x) { + {bar: _, ...const xs} => xs as {foo: 1, baz: 3}, // OK + }; + const out3 = match (x) { + {...const xs} => xs as {foo: 1, bar: 2, baz: 3}, // OK + }; +} + +// Identifier, member, literal +{ + declare const A: 3; + declare const O: {B: 2}; + declare const x: number; + + const out = match (x) { + 0 => 0, + -1 => 0, + +1 => 0, + A => 0, + O.B => 0, + _ => 0, + }; +} + +// Or pattern +{ + declare const x: number; + + const out = match (x) { + 1 | 2 | 3 => 0, + _ => 0, + }; +} + +// Array pattern applied to array literal +{ + const x = [1, 'foo'] as const; + + const out = match (x) { + [_, const b] => b as string, // OK + }; +} + +// BigInt member pattern +{ + declare const x: string; + declare const O: {[bigint]: 'foo'}; + + const out = match (x) { + O[1n] => 0, // OK + _ => 0, + }; +} + +=====================================output===================================== +// Top level binding with \`const\` +{ + declare const x: number; + + const out = match (x) { + const a => a, + }; + out as number; // OK + out as empty; // ERROR +} + +// Binding doesn't leak to outer scope +{ + declare const x: number; + + const out = match (x) { + const a => a, + }; + + a; // ERROR: cannot resolve name +} + +// Array pattern +{ + declare const x: [number]; + + const out = match (x) { + [const a] => a, + }; + out as number; // OK + out as empty; // ERROR +} + +// Object pattern +{ + declare const x: { foo: number }; + + const out = match (x) { + {foo: const a} => a, + }; + + out as number; // OK + out as empty; // ERROR +} + +// Object shorthand pattern +{ + declare const x: { foo: number }; + + const out = match (x) { + {const foo} => foo, + }; + + out as number; // OK + out as empty; // ERROR +} + +// Nested patterns +{ + declare const x: { foo: [{ bar: number }] }; + + const out1 = match (x) { + {foo: [{bar: const a}]} => a, + _ => 0, + }; + out1 as number; // OK + out1 as empty; // ERROR + + const out2 = match (x) { + {foo: const a} => a[0].bar, + }; + out2 as number; // OK + out2 as empty; // ERROR +} + +// Guards +{ + declare const x: { foo: number }; + declare const f: (number) => boolean; + + const out = match (x) { + {foo: const n} if (n === 0) => n, // OK + {foo: const n} if (f(n)) => n, // OK + {foo: const n} if (n as empty) => n, // ERROR + _ => 0, + }; +} + +// As pattern +{ + declare const x: { foo: [number] }; + + const out = match (x) { + {foo: [1] as n} => n as [number], // OK + {foo: [1 as n]} => n as number, // OK + _ => 0, + }; +} +{ + declare const x: { foo: [number] }; + + const out = match (x) { + {foo: [1] as const n} => n as [number], // OK + {foo: [1 as const n]} => n as number, // OK + _ => 0, + }; +} +{ + declare const x: { foo: { bar: number } }; + + const out = match (x) { + {foo: {bar: 1}} => 0, // OK + {foo: {bar: 2} as const a} => 0, // OK + {foo: {bar: 3 as const a}} => 0, // OK + {foo: {bar: const a}} => 0, // OK + _ => 0, + }; +} + +// Array rest +{ + declare const x: [1, 2, 3]; + + const out1 = match (x) { + [1, 2, ...const xs] => xs as [3], // OK + _ => 0, + }; + const out2 = match (x) { + [1, ...const xs] => xs as [2, 3], // OK + _ => 0, + }; + const out3 = match (x) { + [...const xs] => xs as [1, 2, 3], // OK + _ => 0, + }; +} + +// Object rest +{ + declare const x: { foo: 1, bar: 2, baz: 3 }; + + const out1 = match (x) { + {foo: _, bar: _, ...const xs} => xs as { baz: 3 }, // OK + }; + const out2 = match (x) { + {bar: _, ...const xs} => xs as { foo: 1, baz: 3 }, // OK + }; + const out3 = match (x) { + {...const xs} => xs as { foo: 1, bar: 2, baz: 3 }, // OK + }; +} + +// Identifier, member, literal +{ + declare const A: 3; + declare const O: { B: 2 }; + declare const x: number; + + const out = match (x) { + 0 => 0, + -1 => 0, + +1 => 0, + A => 0, + O.B => 0, + _ => 0, + }; +} + +// Or pattern +{ + declare const x: number; + + const out = match (x) { + 1 | 2 | 3 => 0, + _ => 0, + }; +} + +// Array pattern applied to array literal +{ + const x = [1, "foo"] as const; + + const out = match (x) { + [_, const b] => b as string, // OK + }; +} + +// BigInt member pattern +{ + declare const x: string; + declare const O: { [bigint]: "foo" }; + + const out = match (x) { + O[1n] => 0, // OK + _ => 0, + }; +} + +================================================================================ +`; + +exports[`rules-of-hooks.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +declare const x: 1 | 2; + +hook useHook() {} + +component WithMatchStatement() { + match (x) { + 1 => { + useHook(); // ERROR + } + 2 => {} + } + return null +} + +component WithMatchExpression() { + return match (x) { + 1 => useHook(), // ERROR + 2 => undefined, + }; +} + +match (x) { + 1 => { + component NestedInMatch() { + useHook(); // OK + return null; + } + } + 2 => {} +} + +=====================================output===================================== +declare const x: 1 | 2; + +hook useHook() {} + +component WithMatchStatement() { + match (x) { + 1 => { + useHook(); // ERROR + } + 2 => {} + } + return null; +} + +component WithMatchExpression() { + return match (x) { + 1 => useHook(), // ERROR + 2 => undefined, + }; +} + +match (x) { + 1 => { + component NestedInMatch() { + useHook(); // OK + return null; + } + } + 2 => {} +} + +================================================================================ +`; + +exports[`statement.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +{ + declare const x: empty; + match (x) {}; // OK +} + +declare const x: 1 | 2; + +match (x) { + 1 => {} + 2 => {} +} + +function f1() { + match (x) { + 1 => { + throw 0; + } + 2 => { + throw 0; + } + }; + x; // ERROR: unreachable +} + +// Throws in guards +function f2() { + declare function invariant(boolean): empty; + + match (x) { + 1 => {} + 2 if (invariant(false)) => {} + }; + + x; // OK: not unreachable +} + +// Nested matches +{ + match (x) { + 1 => {} + const a => { + match (a) { + _ => {} + } + } + } +} + +// Body makes refinements +{ + let target; + match (x) { + 1 => { + target = "foo"; + } + 2 => { + target = true; + } + } + + target as string | boolean; // OK +} +{ + let target; + match (x) { + const a => { + target = "foo" as const; + } + } + a; // ERROR + + target as "foo"; // OK +} +{ + declare const x: [number] | number; + let target = null; + match (x) { + [const a] => { + target = "foo"; + } + const a => { + target = true; + } + } + a; // ERROR + + target as string | boolean; // OK +} +{ + let target; + match (x) { + 1 => { + target = "foo"; + } + 2 => { + throw 0; + } + } + + target as string; // OK +} +{ + declare const o: {prop: number}; + match (x) { + 1 => { + o.prop = 1; + } + 2 => { + o.prop = 2; + } + } + + o.prop as 1 | 2; // OK +} +{ + const a = []; + match (x) { + 1 => { + a.push(1); + } + 2 => { + a.push(2); + } + } + + a as Array; // OK +} + +// Abnormal exits functions +function t1(): boolean { + match (x) { + 1 => { + return true; + } + 2 => { + return false; + } + } +} + +function t2(): boolean { + match (x) { + 1 => { + throw 0; + } + 2 => { + return false; + } + } +} + +// Abnormals in loops +{ + for (let i = 0; i < 2; i++) { + match (i) { + 0 => { + break; + } + _ => {} + } + } +} +{ + label: for (let i = 0; i < 2; i++) { + match (i) { + 0 => { + break label; + } + _ => {} + } + } +} + +// // Invalid case body +// function t3() { +// match (x) { +// 1 => return 1; // ERROR +// 2 => throw false; // ERROR +// } +// } + +=====================================output===================================== +{ + declare const x: empty; + match (x) { + } // OK +} + +declare const x: 1 | 2; + +match (x) { + 1 => {} + 2 => {} +} + +function f1() { + match (x) { + 1 => { + throw 0; + } + 2 => { + throw 0; + } + } + x; // ERROR: unreachable +} + +// Throws in guards +function f2() { + declare function invariant(boolean): empty; + + match (x) { + 1 => {} + 2 if (invariant(false)) => {} + } + + x; // OK: not unreachable +} + +// Nested matches +{ + match (x) { + 1 => {} + const a => { + match (a) { + _ => {} + } + } + } +} + +// Body makes refinements +{ + let target; + match (x) { + 1 => { + target = "foo"; + } + 2 => { + target = true; + } + } + + target as string | boolean; // OK +} +{ + let target; + match (x) { + const a => { + target = "foo" as const; + } + } + a; // ERROR + + target as "foo"; // OK +} +{ + declare const x: [number] | number; + let target = null; + match (x) { + [const a] => { + target = "foo"; + } + const a => { + target = true; + } + } + a; // ERROR + + target as string | boolean; // OK +} +{ + let target; + match (x) { + 1 => { + target = "foo"; + } + 2 => { + throw 0; + } + } + + target as string; // OK +} +{ + declare const o: { prop: number }; + match (x) { + 1 => { + o.prop = 1; + } + 2 => { + o.prop = 2; + } + } + + o.prop as 1 | 2; // OK +} +{ + const a = []; + match (x) { + 1 => { + a.push(1); + } + 2 => { + a.push(2); + } + } + + a as Array; // OK +} + +// Abnormal exits functions +function t1(): boolean { + match (x) { + 1 => { + return true; + } + 2 => { + return false; + } + } +} + +function t2(): boolean { + match (x) { + 1 => { + throw 0; + } + 2 => { + return false; + } + } +} + +// Abnormals in loops +{ + for (let i = 0; i < 2; i++) { + match (i) { + 0 => { + break; + } + _ => {} + } + } +} +{ + label: for (let i = 0; i < 2; i++) { + match (i) { + 0 => { + break label; + } + _ => {} + } + } +} + +// // Invalid case body +// function t3() { +// match (x) { +// 1 => return 1; // ERROR +// 2 => throw false; // ERROR +// } +// } + +================================================================================ +`; diff --git a/tests/format/flow-repo/match/expression.js b/tests/format/flow-repo/match/expression.js new file mode 100644 index 000000000..3242d8280 --- /dev/null +++ b/tests/format/flow-repo/match/expression.js @@ -0,0 +1,153 @@ +// Type of body and abnormal handling +{ + declare const x: empty; + const out = match (x) {}; + + out as empty; // OK +} + +declare const x: 1 | 2; + +{ + const out = match (x) { + 1 => true, + 2 => false, + }; + + out as boolean; // OK + out as empty; // ERROR +} + +{ + const out = match (x) { + 1 => true, + 2 => 's', + }; + out as boolean | string; // OK + out as empty; // ERROR +} + +declare function invariant(boolean): empty; + +{ + const out = match (x) { + 1 => true, + 2 => invariant(false), + }; + out as boolean; // OK + out as empty; // ERROR +} + +function f1() { + const out = match (x) { + 1 => invariant(false), + 2 => invariant(false), + }; + out; // ERROR: unreachable +} + +// Throws in guards +function f2() { + const out = match (x) { + 1 if (invariant(false)) => true, + _ => 's', + }; + out as string; // OK + out as empty; // ERROR +} + +// Nested matches +{ + const out = match (x) { + 1 => 1, + const a => match (a) { + const a => a, + }, + }; + + out as number; // OK +} + +// Guards can refine values which are not the argument +{ + declare const y: number | string; + + const out = match (x) { + 1 if (typeof y === 'number') => y as number, // OK + const a if (a === 1) => a as 1, + _ => 0, + }; +} + +// Case body provider and refinement analysis +{ + let target; + const out = match (x) { + 1 => target = "foo", + 2 => target = true, + }; + + target as string | boolean; // OK +} +{ + let target; + const out = match (x) { + const a => target = "foo" as const, + }; + a; // ERROR + + target as "foo"; // OK +} +{ + declare const x: [number] | number; + let target = null; + const out = match (x) { + [const a] => target = "foo", + const a => target = true, + }; + a; // ERROR + + target as string | boolean; // OK +} +{ + let target; + const out = match (x) { + 1 => target = "foo", + 2 => invariant(false), + }; + + target as string; // OK +} +{ + declare const o: {prop: number}; + const out = match (x) { + 1 => o.prop = 1, + 2 => o.prop = 2, + }; + + o.prop as 1 | 2; // OK +} +{ + const a = []; + const out = match (x) { + 1 => a.push(1), + 2 => a.push(2), + }; + + a as Array; // OK +} + +// Natural inference hint +{ + const out: 'a' | 'b' = match (x) { + 1 => 'b', // OK + 2 => 'xxx', // ERROR: 'xxx' is not a subtype of 'a' | 'b' + }; +} +{ + declare const dict: {['a' | 'b']: number}; + dict[match (x) { + 1 => 'b', // OK + 2 => 'xxx', // ERROR: 'xxx' is not a subtype of 'a' | 'b' + }]; +} diff --git a/tests/format/flow-repo/match/format.test.js b/tests/format/flow-repo/match/format.test.js new file mode 100644 index 000000000..55c339883 --- /dev/null +++ b/tests/format/flow-repo/match/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ["flow"]); diff --git a/tests/format/flow-repo/match/hints.js b/tests/format/flow-repo/match/hints.js new file mode 100644 index 000000000..002f831d8 --- /dev/null +++ b/tests/format/flow-repo/match/hints.js @@ -0,0 +1,83 @@ +// Annotation hint +type F = string => boolean; +{ + declare const x: 'a'; + + const out: F = match (x) { + 'a' => (y => true), // OK + }; +} + +// Sibling before +{ + declare const x: 'a' | 'b'; + + const out = match (x) { + 'a' => [1], + 'b' => [], // Should be `Array` + }; + out as Array; // OK +} + +// Sibling after +{ + declare const x: 'a' | 'b'; + + const out = match (x) { + 'a' => [], // Should be `Array` + 'b' => [1], + }; + out as Array; // OK +} + +// Multiple siblings, one valid +{ + declare const x: 'a' | 'b' | 'c' | 'd'; + + const out = match (x) { + 'a' => 1, + 'b' => {}, + 'c' => [1], + 'd' => [], // Should be `Array` + }; + out as number | {} | Array; // OK +} + +// Multiple siblings, multiple valid +{ + declare const x: 'a' | 'b' | 'c'; + + const out = match (x) { + 'a' => [true], + 'b' => [1], + 'c' => [], + }; + out as Array | Array; // OK +} +{ + declare const x: 'a' | 'b' | 'c'; + + const out = match (x) { + 'a' => ((x: number) => 1), + 'b' => ((x: string) => true), + 'c' => (x => x as number), // OK + }; +} + +// Cycles avoided +{ + declare const x: 'a' | 'b'; + + const out: (x: number) => void = match (x) { // OK + 'a' => ((x) => {}), + 'b' => ((x) => {}), + }; +} +{ + declare const x: 'a' | 'b'; + + const out: Array = match (x) { // OK + 'a' => [], + 'b' => [], + }; +} diff --git a/tests/format/flow-repo/match/matching.js b/tests/format/flow-repo/match/matching.js new file mode 100644 index 000000000..fa2722d66 --- /dev/null +++ b/tests/format/flow-repo/match/matching.js @@ -0,0 +1,399 @@ +// Primitive value union +{ + declare const x: 1 | -2 | 3n | 's' | false | null; + + const e1 = match (x) { + 1 => 0, + -2 => 0, + 3n => 0, + 's' => 0, + false => 0, + null => 0, + }; + + const e2 = match (x) { // ERROR: not all members checked + 1 => 0, + false => 0, + 3n => 0, + null => 0, + }; +} + +// Identifier patterns +{ + declare const x: 1 | 2; + + declare const one: 1; + declare const two: 2; + + const e1 = match (x) { + one => 0, + two => 0, + }; + + const e2 = match (x) { // ERROR: `2` not checked + one => 0, + }; +} + +// `undefined` +{ + declare const x: 1 | void; + + const e1 = match (x) { + 1 => 0, + undefined => 0, + }; + + const e2 = match (x) { // ERROR: `undefined` not checked + 1 => 0, + }; +} + +// Maybe types +{ + declare const x: ?1; + + const e1 = match (x) { + 1 => 0, + undefined => 0, + null => 0, + }; + + const e2 = match (x) { // ERROR: `null` and `undefined` not checked + 1 => 0, + }; +} + +// Member patterns +{ + declare const x: 1 | 2; + + declare const o: { + one: 1, + two: 2, + }; + + const e1 = match (x) { + o.one => 0, + o.two => 0, + }; + + const e2 = match (x) { // ERROR: `2` not checked + o.one => 0, + }; +} + +// `as` pattern refines using its pattern +{ + declare const x: 1 | 2; + + const e1 = match (x) { + 1 as a => a as 1, // OK + 2 as a => a as 2, // OK + }; +} +{ + declare const x: {type: 'foo', value: 1} + | {type: 'bar', value: 2}; + + // Nested `as` pattern + const e1 = match (x) { + {type: 'foo' as a, const value} => (a as 'foo', value as 1), // OK + {type: 'bar' as a, const value} => (a as 'bar', value as 2), // OK + }; +} + +// Top level binding and wildcard +{ + declare const x: 1 | 2; + + const e1 = match (x) { + 1 => 0, + const a => a as 2, // OK + }; + + const e2 = match (x) { + 1 => 0, + _ => 0, // OK + }; +} + +// Non-ident/member argument still works +{ + declare const f: () => 1 | 2; + + const e1 = match (f()) { + 1 => 0, + 2 => 0, + }; + + const e2 = match (f()) { // ERROR: `2` not checked + 1 => 0, + }; +} + +// Or pattern +{ + declare const x: 1 | 2 | 3; + + const e1 = match (x) { + 1 | 2 | 3 => true, + }; + + const e2 = match (x) { // ERROR: `3` not checked + 1 | 2 => true, + }; +} + +// Patterns with guard could match or not match +{ + declare const x: 1 | 2; + + declare function f(): boolean; + + const e1 = match (x) { // ERROR: `2` not checked + 1 => 0, + 2 if (f()) => 0, + }; + + const e2 = match (x) { + 1 => 0, + 2 if (f()) => 0, + 2 => 0, + }; +} + +// Property exists +{ + declare const x: {foo: void, a: 0} | {bar: void, a: 1}; + + const e1 = match (x) { + {foo: _, const a} => a as 0, // OK + {bar: _, const a} => a as 1, // OK + }; +} +{ + declare const x: + | {foo: void, a: 0} + | {bar: void, a: 1} + | {baz: void, a: 2} + | {zap: void, a: 3}; + + declare const u: void; + declare const o: {u: void}; + + const e1 = match (x) { + {foo: u, const a} => a as 0, // OK + {bar: o.u, const a} => a as 1, // OK + {baz: undefined as v, const a} => a as 2, // OK + {zap: u, const a} => a as 3, // OK + }; +} + +// Disjoint object union +{ + declare const x: {type: 'foo', val: number} + | {type: 'bar', val: string} + | {type: 'baz', val: boolean}; + + const e1 = match (x) { + {type: 'foo', val: const a} => a as number, // OK + {type: 'bar', val: const a} => a as string, // OK + {type: 'baz', val: const a} => a as boolean, // OK + }; + + const e2 = match (x) { // ERROR: `type: 'baz'` not checked + {type: 'foo', val: const a} => a as number, // OK + {type: 'bar', val: const a} => a as string, // OK + }; + + // Using idents as pattern + declare const foo: 'foo'; + declare const bar: 'bar'; + declare const baz: 'baz'; + const e3 = match (x) { + {type: foo, val: const a} => a as number, // OK + {type: bar, val: const a} => a as string, // OK + {type: baz, val: const a} => a as boolean, // OK + }; + + // Using members as pattern + declare const o: { + foo: 'foo', + bar: 'bar', + baz: 'baz', + }; + const e4 = match (x) { + {type: o.foo, val: const a} => a as number, // OK + {type: o.bar, val: const a} => a as string, // OK + {type: o.baz, val: const a} => a as boolean, // OK + }; +} + +// Disjoint object union with multiple pivot props +{ + declare const x: {type: 'foo', val: number} + | {type: 'bar', n: 1, val: string} + | {type: 'bar', n: 2, val: boolean}; + + const e1 = match (x) { + {type: 'foo', val: const a} => a as number, // OK + {type: 'bar', val: const a, ...} => a as string | boolean, // OK + }; + + const e2 = match (x) { + {type: 'foo', val: const a} => a as number, // OK + {type: 'bar', n: 1, val: const a} => a as string, // OK + {type: 'bar', n: 2, val: const a} => a as boolean, // OK + }; + + const e3 = match (x) { // ERROR: `type: 'bar', n: 2` not checked + {type: 'foo', val: const a} => a as number, // OK + {type: 'bar', n: 1, val: const a} => a as string, // OK + }; +} + +// Combo union of object with sentinel property and primitive value +{ + declare const x: null | {type: 'bar', val: number}; + + const e1 = match (x) { + {type: 'bar', val: const a} => a as number, // OK + null => 0, + }; +} + +// Or pattern: objects +{ + declare const x: {type: 'foo', val: number} + | {type: 'bar', val: string} + | {type: 'baz', val: boolean}; + + const e1 = match (x) { + {type: 'foo', ...} | {type: 'bar', ...} | {type: 'baz', ...} => 0, + }; + + const e2 = match (x) { // ERROR: `type: 'bar'` not checked + {type: 'foo', ...} | {type: 'baz', ...} => 0, + }; +} + +// Disjoint tuple union +{ + declare const x: ['foo', number] + | ['bar', string] + | ['baz', boolean]; + + const e1 = match (x) { + ['foo', const a] => a as number, // OK + ['bar', const a] => a as string, // OK + ['baz', const a] => a as boolean, // OK + }; + + const e2 = match (x) { // ERROR: `'baz'` element not checked + ['foo', const a] => a as number, // OK + ['bar', const a] => a as string, // OK + }; + + // Using idents as pattern + declare const foo: 'foo'; + declare const bar: 'bar'; + declare const baz: 'baz'; + const e3 = match (x) { + [foo, const a] => a as number, // OK + [bar, const a] => a as string, // OK + [baz, const a] => a as boolean, // OK + }; +} + +// Combo union of tuples with sentinel property and primitive value +{ + declare const x: null | ['bar', number] | ['foo', string]; + + const e1 = match (x) { + ['bar', const a] => a as number, // OK + ['foo', const a] => a as string, // OK + null => 0, + }; +} + +// Tuple length refinements +{ + declare const x: [number] + | [string, string] + | [boolean, boolean, boolean]; + + const e1 = match (x) { + [const a] => a as number, // OK + [const a, _] => a as string, // OK + [const a, _, _] => a as empty, // ERROR: `boolean` is not `empty` + }; + + const e2 = match (x) { + [...] => 0, // OK: matches all + } + + const e3 = match (x) { + [const a, _, ...] => a as string | boolean, // OK + [const a] => a as number, // OK + } +} +{ + declare const x: [number] | Array; + + const e1 = match (x) { + [] => 0, // OK + [const a] => a as string, // ERROR: `number` is not `string` + [const a, _] => a as string, // OK + const d => d as Array, // OK: tuple checked, but array could have other lengths + }; + + const e2 = match (x) { + [...] => 0, // OK: matches all + } +} + +// Optional tuple elements +{ + declare const x: [a: 0, b?: 1, c?: 2]; + + const e1 = match (x) { + [_, ...] => 0, + }; + + const e2 = match (x) { // ERROR: does not match all possibilities + [_, _, ...] => 0, + }; + + const e3 = match (x) { // ERROR: does not match all possibilities + [_] => 0, + }; + + const e4 = match (x) { // ERROR: does not match all possibilities + [_, _, _] => 0, + }; +} + +// Inexact tuple types +{ + declare const x: [a: 0, ...]; + + const e1 = match (x) { + [_, ...] => 0, + }; + + const e2 = match (x) { // ERROR: does not match all elements + [_] => 0, + }; +} + +// Exhaustive checking error points to definition +{ + type T = {foo: 1} | {foo: 2}; + declare const x: T; + + const e1 = match (x) { + {foo: 1} => 0, + }; +} diff --git a/tests/format/flow-repo/match/pattern-errors.js b/tests/format/flow-repo/match/pattern-errors.js new file mode 100644 index 000000000..2ac7a9c71 --- /dev/null +++ b/tests/format/flow-repo/match/pattern-errors.js @@ -0,0 +1,191 @@ +// 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 + }; +} + +// Invalid numeric property +{ + declare const x: {1: true}; + + const e1 = match (x) { + {1.1: _} => 0, // ERROR + _ => 0, + }; +} + +// Unary pattern on `0` banned +{ + declare const x: 0; + + const e1 = match (x) { + -0 => true, // ERROR + +0 => true, // ERROR + 0 => true, // OK + }; +} + +// Unary `+` on bigint banned +{ + declare const x: 1n; + + const e1 = match (x) { + +1n => true, // ERROR + 1n => true, // OK + }; +} + +// Duplicate object keys banned +{ + declare const x: {foo: boolean}; + + const e1 = match (x) { + {foo: true, foo: false} => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + {foo: true, const foo} => 0, // ERROR + _ => 0, + }; + + const e3 = match (x) { + {foo: true, 'foo': false} => 0, // ERROR + _ => 0, + }; +} + +// Duplicate binding names +{ + declare const x: [boolean, boolean]; + + const e1 = match (x) { + [const a, true as a] => 0, // ERROR + [const a, const a] => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + [const a, true as const a] => 0, // ERROR + [const a, ...const a] => 0, // ERROR + _ => 0, + }; +} +{ + declare const x: {a: boolean, b: boolean}; + const e1 = match (x) { + {const a, b: true as a} => 0, // ERROR + {const a, ...const a} => 0, // ERROR + _ => 0, + }; +} + +// Bindings in 'or' patterns are not yet supported +{ + declare const x: [boolean] | [string]; + + const e1 = match (x) { + [true as a] | ['s'] => 0, // ERROR + [false as const a] | ['s'] => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + [true as const a] | ['s'] => 0, // ERROR + ['t'] | [...const a] => 0, // ERROR + _ => 0, + }; +} +{ + declare const x: {a: boolean} | {a: string}; + + const e1 = match (x) { + {a: true as a} | {a: 's'} => 0, // ERROR + {a: false as const a} | {a: 's'} => 0, // ERROR + _ => 0, + }; +} + +// As pattern on binding pattern +{ + declare const x: [boolean]; + + const e1 = match (x) { + [const a as b] => 0, // ERROR + _ => 0, + }; + + const e2 = match (x) { + const a as const b => 0, // ERROR + }; +} + +// Reference before declaration +{ + declare const x: [number, number]; + + const out = match (x) { + [a, const a] => a, // ERROR: reference before declaration + _ => 0, + }; +} +{ + declare const x: [number, {foo: number}]; + + const out = match (x) { + [a.foo, const a] => a, // ERROR: reference before declaration + _ => 0, + }; +} + +// Reference binding introduced in same pattern +{ + declare const x: [number, number]; + + const out = match (x) { + [const a, a] => a, // ERROR + _ => 0, + }; +} +{ + declare const x: [{foo: number}, number]; + + const out = match (x) { + [const a, a.foo] => a, // ERROR + _ => 0, + }; +} + +// // Invalid object property shorthand +// { +// declare const x: {foo: 1, bar: boolean}; + +// const out = match (x) { +// {foo} => 0, // ERROR +// {foo, bar: true} => 0, // ERROR +// _ => 0, +// }; +// } + +// BigInt property usage +{ + declare const x: mixed; + + const out = match (x) { + {1n: 1, ...} => 0, // ERROR + _ => 0, + }; +} diff --git a/tests/format/flow-repo/match/patterns.js b/tests/format/flow-repo/match/patterns.js new file mode 100644 index 000000000..243271087 --- /dev/null +++ b/tests/format/flow-repo/match/patterns.js @@ -0,0 +1,197 @@ +// Top level binding with `const` +{ + declare const x: number; + + const out = match (x) { + const a => a, + }; + out as number; // OK + out as empty; // ERROR +} + +// Binding doesn't leak to outer scope +{ + declare const x: number; + + const out = match (x) { + const a => a, + }; + + a; // ERROR: cannot resolve name +} + +// Array pattern +{ + declare const x: [number]; + + const out = match (x) { + [const a] => a, + }; + out as number; // OK + out as empty; // ERROR +} + +// Object pattern +{ + declare const x: {foo: number}; + + const out = match (x) { + {foo: const a} => a, + }; + + out as number; // OK + out as empty; // ERROR +} + +// Object shorthand pattern +{ + declare const x: {foo: number}; + + const out = match (x) { + {const foo} => foo, + }; + + out as number; // OK + out as empty; // ERROR +} + +// Nested patterns +{ + declare const x: {foo: [{bar: number}]}; + + const out1 = match (x) { + {foo: [{bar: const a}]} => a, + _ => 0, + }; + out1 as number; // OK + out1 as empty; // ERROR + + const out2 = match (x) { + {foo: const a} => a[0].bar, + }; + out2 as number; // OK + out2 as empty; // ERROR +} + +// Guards +{ + declare const x: {foo: number}; + declare const f: number => boolean; + + const out = match (x) { + {foo: const n} if (n === 0) => n, // OK + {foo: const n} if (f(n)) => n, // OK + {foo: const n} if (n as empty) => n, // ERROR + _ => 0, + }; +} + +// As pattern +{ + declare const x: {foo: [number]}; + + const out = match (x) { + {foo: [1] as n} => n as [number], // OK + {foo: [1 as n]} => n as number, // OK + _ => 0, + }; +} +{ + declare const x: {foo: [number]}; + + const out = match (x) { + {foo: [1] as const n} => n as [number], // OK + {foo: [1 as const n]} => n as number, // OK + _ => 0, + }; +} +{ + declare const x: {foo: {bar: number}}; + + const out = match (x) { + {foo: {bar: 1}} => 0, // OK + {foo: {bar: 2} as const a} => 0, // OK + {foo: {bar: 3 as const a}} => 0, // OK + {foo: {bar: const a}} => 0, // OK + _ => 0, + }; +} + +// Array rest +{ + declare const x: [1, 2, 3]; + + const out1 = match (x) { + [1, 2, ...const xs] => xs as [3], // OK + _ => 0, + }; + const out2 = match (x) { + [1, ...const xs] => xs as [2, 3], // OK + _ => 0, + }; + const out3 = match (x) { + [...const xs] => xs as [1, 2, 3], // OK + _ => 0, + }; +} + +// Object rest +{ + declare const x: {foo: 1, bar: 2, baz: 3}; + + const out1 = match (x) { + {foo: _, bar: _, ...const xs} => xs as {baz: 3}, // OK + }; + const out2 = match (x) { + {bar: _, ...const xs} => xs as {foo: 1, baz: 3}, // OK + }; + const out3 = match (x) { + {...const xs} => xs as {foo: 1, bar: 2, baz: 3}, // OK + }; +} + +// Identifier, member, literal +{ + declare const A: 3; + declare const O: {B: 2}; + declare const x: number; + + const out = match (x) { + 0 => 0, + -1 => 0, + +1 => 0, + A => 0, + O.B => 0, + _ => 0, + }; +} + +// Or pattern +{ + declare const x: number; + + const out = match (x) { + 1 | 2 | 3 => 0, + _ => 0, + }; +} + +// Array pattern applied to array literal +{ + const x = [1, 'foo'] as const; + + const out = match (x) { + [_, const b] => b as string, // OK + }; +} + +// BigInt member pattern +{ + declare const x: string; + declare const O: {[bigint]: 'foo'}; + + const out = match (x) { + O[1n] => 0, // OK + _ => 0, + }; +} diff --git a/tests/format/flow-repo/match/rules-of-hooks.js b/tests/format/flow-repo/match/rules-of-hooks.js new file mode 100644 index 000000000..e48254a5d --- /dev/null +++ b/tests/format/flow-repo/match/rules-of-hooks.js @@ -0,0 +1,30 @@ +declare const x: 1 | 2; + +hook useHook() {} + +component WithMatchStatement() { + match (x) { + 1 => { + useHook(); // ERROR + } + 2 => {} + } + return null +} + +component WithMatchExpression() { + return match (x) { + 1 => useHook(), // ERROR + 2 => undefined, + }; +} + +match (x) { + 1 => { + component NestedInMatch() { + useHook(); // OK + return null; + } + } + 2 => {} +} diff --git a/tests/format/flow-repo/match/statement.js b/tests/format/flow-repo/match/statement.js new file mode 100644 index 000000000..76029ae41 --- /dev/null +++ b/tests/format/flow-repo/match/statement.js @@ -0,0 +1,180 @@ +{ + declare const x: empty; + match (x) {}; // OK +} + +declare const x: 1 | 2; + +match (x) { + 1 => {} + 2 => {} +} + +function f1() { + match (x) { + 1 => { + throw 0; + } + 2 => { + throw 0; + } + }; + x; // ERROR: unreachable +} + +// Throws in guards +function f2() { + declare function invariant(boolean): empty; + + match (x) { + 1 => {} + 2 if (invariant(false)) => {} + }; + + x; // OK: not unreachable +} + +// Nested matches +{ + match (x) { + 1 => {} + const a => { + match (a) { + _ => {} + } + } + } +} + +// Body makes refinements +{ + let target; + match (x) { + 1 => { + target = "foo"; + } + 2 => { + target = true; + } + } + + target as string | boolean; // OK +} +{ + let target; + match (x) { + const a => { + target = "foo" as const; + } + } + a; // ERROR + + target as "foo"; // OK +} +{ + declare const x: [number] | number; + let target = null; + match (x) { + [const a] => { + target = "foo"; + } + const a => { + target = true; + } + } + a; // ERROR + + target as string | boolean; // OK +} +{ + let target; + match (x) { + 1 => { + target = "foo"; + } + 2 => { + throw 0; + } + } + + target as string; // OK +} +{ + declare const o: {prop: number}; + match (x) { + 1 => { + o.prop = 1; + } + 2 => { + o.prop = 2; + } + } + + o.prop as 1 | 2; // OK +} +{ + const a = []; + match (x) { + 1 => { + a.push(1); + } + 2 => { + a.push(2); + } + } + + a as Array; // OK +} + +// Abnormal exits functions +function t1(): boolean { + match (x) { + 1 => { + return true; + } + 2 => { + return false; + } + } +} + +function t2(): boolean { + match (x) { + 1 => { + throw 0; + } + 2 => { + return false; + } + } +} + +// Abnormals in loops +{ + for (let i = 0; i < 2; i++) { + match (i) { + 0 => { + break; + } + _ => {} + } + } +} +{ + label: for (let i = 0; i < 2; i++) { + match (i) { + 0 => { + break label; + } + _ => {} + } + } +} + +// // Invalid case body +// function t3() { +// match (x) { +// 1 => return 1; // ERROR +// 2 => throw false; // ERROR +// } +// } diff --git a/tests/format/flow/match/from-hermes-repo/__snapshots__/format.test.js.snap b/tests/format/flow/match/from-hermes-repo/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..67f407356 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/__snapshots__/format.test.js.snap @@ -0,0 +1,416 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`export.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +export default match (a) {} + +=====================================output===================================== +export default match (a) { +}; + +================================================================================ +`; + +exports[`expression.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +const e = match (a) { + 1 => true, + 'foo' => false, + 2 => {obj: 'literal'}, +}; + +=====================================output===================================== +const e = match (a) { + 1 => true, + "foo" => false, + 2 => { obj: "literal" }, +}; + +================================================================================ +`; + +exports[`expression-empty.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +const e = match (a) {}; + +=====================================output===================================== +const e = match (a) { +}; + +================================================================================ +`; + +exports[`expression-guards.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +const e = match (a) { + 1 if (b) => true, +}; + +=====================================output===================================== +const e = match (a) { + 1 if (b) => true, +}; + +================================================================================ +`; + +exports[`pattern-array.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +const e = match (x) { + [10] => 1, + [const y, 1] => y, + [1, ...] => 1, + [1, 2, ...const rest] => rest, + [...let rest] => rest, + [...var rest] => rest, + [{nested: [1, const x]}] => x, +}; + +=====================================output===================================== +const e = match (x) { + [10] => 1, + [const y, 1] => y, + [1, ...] => 1, + [1, 2, ...const rest] => rest, + [...let rest] => rest, + [...var rest] => rest, + [{nested: [1, const x]}] => x, +}; + +================================================================================ +`; + +exports[`pattern-core.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +const e = match (x) { + "s" => 1, + true => 1, + null => 1, + 3 => 1, + 4n => 1, + +5 => 1, + -6 => 1, + +7n => 1, + -8n => 1, + y => 1, + const y => y, + let y => y, + var y => y, + ('s') => 1, + _ => 1, +}; + +=====================================output===================================== +const e = match (x) { + "s" => 1, + true => 1, + null => 1, + 3 => 1, + 4n => 1, + +5 => 1, + -6 => 1, + +7n => 1, + -8n => 1, + y => 1, + const y => y, + let y => y, + var y => y, + "s" => 1, + _ => 1, +}; + +================================================================================ +`; + +exports[`pattern-member.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +const e = match (x) { + foo.bar => true, + foo[1] => true, + foo[1n] => true, + foo["bar"] => true, + foo.bar[1] => true, + foo[1].bar["baz"] => true, +}; + +=====================================output===================================== +const e = match (x) { + foo.bar => true, + foo[1] => true, + foo[1n] => true, + foo["bar"] => true, + foo.bar[1] => true, + foo[1].bar["baz"] => true, +}; + +================================================================================ +`; + +exports[`pattern-object.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +const e = match (x) { + {foo: 1, bar: 2} => 1, + {'foo': 1} => 1, + {111: true} => 1, + {foo: const y} => y, + {const x, let y, var z} => y, + {const x, ...const y} => y, + {const x, ...let y} => y, + {const x, ...} => x, +}; + +=====================================output===================================== +const e = match (x) { + {foo: 1, bar: 2} => 1, + {"foo": 1} => 1, + {111: true} => 1, + {foo: const y} => y, + {const x, let y, var z} => y, + {const x, ...const y} => y, + {const x, ...let y} => y, + {const x, ...} => x, +}; + +================================================================================ +`; + +exports[`pattern-or-as.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +const e = match (x) { + "s" | true | null => 1, + | "s" + | true + | null => 1, + {foo: 1 | 2} => 2, + {foo: | 1 + | 2} => 2, + {foo: [1] as y} => y, + {foo: 1 | 2 | 3 as y} => y, + {foo: (1 | 2 | 3) as y} => y, + {foo: [1] as const y} => y, +}; + +=====================================output===================================== +const e = match (x) { + "s" | true | null => 1, + "s" | true | null => 1, + {foo: 1 | 2} => 2, + {foo: 1 | 2} => 2, + {foo: [1] as y} => y, + {foo: (1 | 2 | 3) as y} => y, + {foo: (1 | 2 | 3) as y} => y, + {foo: [1] as const y} => y, +}; + +================================================================================ +`; + +exports[`regression.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +type match = number; +{ + const match = 1; +} +function match(match: match) {} +const a = match(1); +const b = match(1).f(); +const c = match(1) +{ + // block statement +} +const d = match(1) + 2; +const e = match(1)[2]; +const f = match(1) ? 2 : 3; +const g = match(1) < 2; +const h = match(1) && 2; +const i = match(); +const j = match(...b); +match(1); +match(1).f(); +match(1) +{ + // block statement +} +match(1) + 2; +match(1)[2]; +match(1) ? 2 : 3; +match(1) < 2; +match(1) && 2; +match(); +match(...b); + +=====================================output===================================== +type match = number; +{ + const match = 1; +} +function match(match: match) {} +const a = match(1); +const b = match(1).f(); +const c = match(1); +{ + // block statement +} +const d = match(1) + 2; +const e = match(1)[2]; +const f = match(1) ? 2 : 3; +const g = match(1) < 2; +const h = match(1) && 2; +const i = match(); +const j = match(...b); +match(1); +match(1).f(); +match(1); +{ + // block statement +} +match(1) + 2; +match(1)[2]; +match(1) ? 2 : 3; +match(1) < 2; +match(1) && 2; +match(); +match(...b); + +================================================================================ +`; + +exports[`statement.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +match (a) { + 1 => { + const x = 1; + f(x); + }, + 'foo' => { + // empty + }, + 2 => { + label: 'literal' + }, +} + +=====================================output===================================== +match (a) { + 1 => { + const x = 1; + f(x); + } + "foo" => { + // empty + } + 2 => { + label: "literal"; + } +} + +================================================================================ +`; + +exports[`statement-empty.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +match (a) {} + +=====================================output===================================== +match (a) { +} + +================================================================================ +`; + +exports[`statement-guards.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +match (a) { + 1 if (b) => { + const x = 1; + }, + 'foo' if (f()) => { + const x = 2; + }, + 2 if (x < y) => { + const x = 3; + }, +} + +=====================================output===================================== +match (a) { + 1 if (b) => { + const x = 1; + } + "foo" if (f()) => { + const x = 2; + } + 2 if (x < y) => { + const x = 3; + } +} + +================================================================================ +`; + +exports[`statement-sequence-arg.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +match (a, b) {} + +=====================================output===================================== +match ((a, b)) { +} + +================================================================================ +`; diff --git a/tests/format/flow/match/from-hermes-repo/export.js b/tests/format/flow/match/from-hermes-repo/export.js new file mode 100644 index 000000000..1cd6c0158 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/export.js @@ -0,0 +1 @@ +export default match (a) {} diff --git a/tests/format/flow/match/from-hermes-repo/expression-empty.js b/tests/format/flow/match/from-hermes-repo/expression-empty.js new file mode 100644 index 000000000..8961c9da1 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/expression-empty.js @@ -0,0 +1 @@ +const e = match (a) {}; diff --git a/tests/format/flow/match/from-hermes-repo/expression-guards.js b/tests/format/flow/match/from-hermes-repo/expression-guards.js new file mode 100644 index 000000000..c12d76374 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/expression-guards.js @@ -0,0 +1,3 @@ +const e = match (a) { + 1 if (b) => true, +}; diff --git a/tests/format/flow/match/from-hermes-repo/expression.js b/tests/format/flow/match/from-hermes-repo/expression.js new file mode 100644 index 000000000..26c2c7900 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/expression.js @@ -0,0 +1,5 @@ +const e = match (a) { + 1 => true, + 'foo' => false, + 2 => {obj: 'literal'}, +}; diff --git a/tests/format/flow/match/from-hermes-repo/format.test.js b/tests/format/flow/match/from-hermes-repo/format.test.js new file mode 100644 index 000000000..5611d7ab6 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/format.test.js @@ -0,0 +1,2 @@ +// Tests taken from https://github.com/facebook/hermes/tree/83c8115/test/Parser/flow/match +runFormatTest(import.meta, ["flow"]); diff --git a/tests/format/flow/match/from-hermes-repo/pattern-array.js b/tests/format/flow/match/from-hermes-repo/pattern-array.js new file mode 100644 index 000000000..a73e66196 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/pattern-array.js @@ -0,0 +1,9 @@ +const e = match (x) { + [10] => 1, + [const y, 1] => y, + [1, ...] => 1, + [1, 2, ...const rest] => rest, + [...let rest] => rest, + [...var rest] => rest, + [{nested: [1, const x]}] => x, +}; diff --git a/tests/format/flow/match/from-hermes-repo/pattern-core.js b/tests/format/flow/match/from-hermes-repo/pattern-core.js new file mode 100644 index 000000000..32bc9c7f9 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/pattern-core.js @@ -0,0 +1,17 @@ +const e = match (x) { + "s" => 1, + true => 1, + null => 1, + 3 => 1, + 4n => 1, + +5 => 1, + -6 => 1, + +7n => 1, + -8n => 1, + y => 1, + const y => y, + let y => y, + var y => y, + ('s') => 1, + _ => 1, +}; diff --git a/tests/format/flow/match/from-hermes-repo/pattern-member.js b/tests/format/flow/match/from-hermes-repo/pattern-member.js new file mode 100644 index 000000000..cc1f92233 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/pattern-member.js @@ -0,0 +1,8 @@ +const e = match (x) { + foo.bar => true, + foo[1] => true, + foo[1n] => true, + foo["bar"] => true, + foo.bar[1] => true, + foo[1].bar["baz"] => true, +}; diff --git a/tests/format/flow/match/from-hermes-repo/pattern-object.js b/tests/format/flow/match/from-hermes-repo/pattern-object.js new file mode 100644 index 000000000..ddb18ace6 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/pattern-object.js @@ -0,0 +1,10 @@ +const e = match (x) { + {foo: 1, bar: 2} => 1, + {'foo': 1} => 1, + {111: true} => 1, + {foo: const y} => y, + {const x, let y, var z} => y, + {const x, ...const y} => y, + {const x, ...let y} => y, + {const x, ...} => x, +}; diff --git a/tests/format/flow/match/from-hermes-repo/pattern-or-as.js b/tests/format/flow/match/from-hermes-repo/pattern-or-as.js new file mode 100644 index 000000000..fb56e0406 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/pattern-or-as.js @@ -0,0 +1,13 @@ +const e = match (x) { + "s" | true | null => 1, + | "s" + | true + | null => 1, + {foo: 1 | 2} => 2, + {foo: | 1 + | 2} => 2, + {foo: [1] as y} => y, + {foo: 1 | 2 | 3 as y} => y, + {foo: (1 | 2 | 3) as y} => y, + {foo: [1] as const y} => y, +}; diff --git a/tests/format/flow/match/from-hermes-repo/regression.js b/tests/format/flow/match/from-hermes-repo/regression.js new file mode 100644 index 000000000..fe48bc7d1 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/regression.js @@ -0,0 +1,31 @@ +type match = number; +{ + const match = 1; +} +function match(match: match) {} +const a = match(1); +const b = match(1).f(); +const c = match(1) +{ + // block statement +} +const d = match(1) + 2; +const e = match(1)[2]; +const f = match(1) ? 2 : 3; +const g = match(1) < 2; +const h = match(1) && 2; +const i = match(); +const j = match(...b); +match(1); +match(1).f(); +match(1) +{ + // block statement +} +match(1) + 2; +match(1)[2]; +match(1) ? 2 : 3; +match(1) < 2; +match(1) && 2; +match(); +match(...b); diff --git a/tests/format/flow/match/from-hermes-repo/statement-empty.js b/tests/format/flow/match/from-hermes-repo/statement-empty.js new file mode 100644 index 000000000..129d1fe42 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/statement-empty.js @@ -0,0 +1 @@ +match (a) {} diff --git a/tests/format/flow/match/from-hermes-repo/statement-guards.js b/tests/format/flow/match/from-hermes-repo/statement-guards.js new file mode 100644 index 000000000..80319148c --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/statement-guards.js @@ -0,0 +1,11 @@ +match (a) { + 1 if (b) => { + const x = 1; + }, + 'foo' if (f()) => { + const x = 2; + }, + 2 if (x < y) => { + const x = 3; + }, +} diff --git a/tests/format/flow/match/from-hermes-repo/statement-sequence-arg.js b/tests/format/flow/match/from-hermes-repo/statement-sequence-arg.js new file mode 100644 index 000000000..9c645b893 --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/statement-sequence-arg.js @@ -0,0 +1 @@ +match (a, b) {} diff --git a/tests/format/flow/match/from-hermes-repo/statement.js b/tests/format/flow/match/from-hermes-repo/statement.js new file mode 100644 index 000000000..5d41744bf --- /dev/null +++ b/tests/format/flow/match/from-hermes-repo/statement.js @@ -0,0 +1,12 @@ +match (a) { + 1 => { + const x = 1; + f(x); + }, + 'foo' => { + // empty + }, + 2 => { + label: 'literal' + }, +} diff --git a/tests/format/flow/type-parameters/flow-only/format.test.js b/tests/format/flow/type-parameters/flow-only/format.test.js index 370552fb8..55c339883 100644 --- a/tests/format/flow/type-parameters/flow-only/format.test.js +++ b/tests/format/flow/type-parameters/flow-only/format.test.js @@ -1,5 +1 @@ -runFormatTest(import.meta, ["flow"], { - errors: { - "babel-flow": ["const-type-params.js"], - }, -}); +runFormatTest(import.meta, ["flow"]); diff --git a/tests/format/misc/errors/flow/invalid-match/__snapshots__/format.test.js.snap b/tests/format/misc/errors/flow/invalid-match/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..fd7acb4cc --- /dev/null +++ b/tests/format/misc/errors/flow/invalid-match/__snapshots__/format.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`snippet: #0 [flow] format 1`] = ` +"\`match\` argument must not be empty (1:7) +> 1 | match () {} + | ^^" +`; + +exports[`snippet: #0 [hermes] format 1`] = ` +"'match' argument must not be empty (1:6) +> 1 | match () {} + | ^ +Cause: 'match' argument must not be empty (1:6) +match () {} + ^~" +`; + +exports[`snippet: #1 [flow] format 1`] = ` +"\`match\` argument cannot contain spread elements (1:8) +> 1 | match (...b) {} + | ^^^^" +`; + +exports[`snippet: #1 [hermes] format 1`] = ` +"'match' argument cannot contain spread elements (1:7) +> 1 | match (...b) {} + | ^ +Cause: 'match' argument cannot contain spread elements (1:7) +match (...b) {} + ^~~~" +`; diff --git a/tests/format/misc/errors/flow/invalid-match/format.test.js b/tests/format/misc/errors/flow/invalid-match/format.test.js new file mode 100644 index 000000000..90b24dc5c --- /dev/null +++ b/tests/format/misc/errors/flow/invalid-match/format.test.js @@ -0,0 +1,12 @@ +runFormatTest( + { + importMeta: import.meta, + snippets: [ + // https://github.com/facebook/hermes/blob/83c811545b35dc2284ba27786314936e4c80ee6a/test/Parser/flow/match/statement-no-arg.js + "match () {}", + // https://github.com/facebook/hermes/blob/83c811545b35dc2284ba27786314936e4c80ee6a/test/Parser/flow/match/statement-spread-arg.js + "match (...b) {}", + ], + }, + ["flow", "hermes"], +); diff --git a/tests/unit/__snapshots__/visitor-keys.js.snap b/tests/unit/__snapshots__/visitor-keys.js.snap index 35763cb93..ed431b694 100644 --- a/tests/unit/__snapshots__/visitor-keys.js.snap +++ b/tests/unit/__snapshots__/visitor-keys.js.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`visitor keys estree 1`] = ` { @@ -523,6 +523,63 @@ exports[`visitor keys estree 1`] = ` "left", "right", ], + "MatchArrayPattern": [ + "elements", + "rest", + ], + "MatchAsPattern": [ + "pattern", + "target", + ], + "MatchBindingPattern": [ + "id", + ], + "MatchExpression": [ + "argument", + "cases", + ], + "MatchExpressionCase": [ + "body", + "guard", + "pattern", + ], + "MatchIdentifierPattern": [ + "id", + ], + "MatchLiteralPattern": [ + "literal", + ], + "MatchMemberPattern": [ + "base", + "property", + ], + "MatchObjectPattern": [ + "properties", + "rest", + ], + "MatchObjectPatternProperty": [ + "key", + "pattern", + ], + "MatchOrPattern": [ + "patterns", + ], + "MatchRestPattern": [ + "argument", + ], + "MatchStatement": [ + "argument", + "cases", + ], + "MatchStatementCase": [ + "body", + "guard", + "pattern", + ], + "MatchUnaryPattern": [ + "argument", + ], + "MatchWildcardPattern": [], "MemberExpression": [ "object", "property",