diff --git a/.gitignore b/.gitignore index 0853b5ab2..d85815f18 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ package-lock.json .node-version # When installing software on gitpod.io, `core.*` files are generated /core.* +*.swp # For 0x flamegraph directory (https://github.com/davidmarkclements/0x) *.0x/ diff --git a/changelog_unreleased/flow/14619.md b/changelog_unreleased/flow/14619.md new file mode 100644 index 000000000..224afe2d4 --- /dev/null +++ b/changelog_unreleased/flow/14619.md @@ -0,0 +1,12 @@ +#### Support Mapped Types and keyof (#14619 by @jbrown215) + +```jsx +// Input +type Mapped = {[key in keyof O]:number}; + +// Prettier stable +// Does not parse + +// Prettier main +type Mapped = { [key in keyof O]: number }; +``` diff --git a/cspell.json b/cspell.json index 2a58e552c..4a0d8afd1 100644 --- a/cspell.json +++ b/cspell.json @@ -326,6 +326,7 @@ "tldr", "Tomasek", "toplevel", + "Tparam", "Tradeshift", "Transloadit", "trippable", diff --git a/src/language-js/print/flow.js b/src/language-js/print/flow.js index 1cade1bb5..93f0550c1 100644 --- a/src/language-js/print/flow.js +++ b/src/language-js/print/flow.js @@ -44,6 +44,7 @@ import { printDeclareToken, } from "./misc.js"; import { printTernary } from "./ternary.js"; +import { printFlowMappedTypeProperty } from "./mapped-type.js"; function printFlow(path, options, print) { const { node } = path; @@ -178,9 +179,12 @@ function printFlow(path, options, print) { assert.ok(kind === "plus" || kind === "minus"); return kind === "plus" ? "+" : "-"; } + case "KeyofTypeAnnotation": + return ["keyof ", print("argument")]; case "ObjectTypeCallProperty": return [node.static ? "static " : "", print("value")]; - + case "ObjectTypeMappedTypeProperty": + return printFlowMappedTypeProperty(path, options, print); case "ObjectTypeIndexer": return [ node.static ? "static " : "", diff --git a/src/language-js/print/mapped-type.js b/src/language-js/print/mapped-type.js new file mode 100644 index 000000000..d8411b7d1 --- /dev/null +++ b/src/language-js/print/mapped-type.js @@ -0,0 +1,88 @@ +import { printDanglingComments } from "../../main/comments/print.js"; +import hasNewlineInRange from "../../utils/has-newline-in-range.js"; +import { locStart, locEnd } from "../loc.js"; +import { + group, + softline, + indent, + ifBreak, + line, +} from "../../document/builders.js"; + +/** + * @param {string | null} optional + * @returns {string} + */ +function printFlowMappedTypeOptionalModifier(optional) { + switch (optional) { + case null: + return ""; + case "PlusOptional": + return "+?"; + case "MinusOptional": + return "-?"; + case "Optional": + return "?"; + } +} + +function printFlowMappedTypeProperty(path, options, print) { + const { node } = path; + return group([ + node.variance ? print("variance") : "", + "[", + indent([print("keyTparam"), " in ", print("sourceType")]), + "]", + printFlowMappedTypeOptionalModifier(node.optional), + ": ", + print("propType"), + ]); +} + +/** + * @param {string} tokenNode + * @param {string} keyword + * @returns {string} + */ +function printTypeScriptMappedTypeModifier(tokenNode, keyword) { + if (tokenNode === "+" || tokenNode === "-") { + return tokenNode + keyword; + } + + return keyword; +} + +function printTypescriptMappedType(path, options, print) { + const { node } = path; + const shouldBreak = hasNewlineInRange( + options.originalText, + locStart(node), + locEnd(node) + ); + + return group( + [ + "{", + indent([ + options.bracketSpacing ? line : softline, + print("typeParameter"), + node.optional + ? printTypeScriptMappedTypeModifier(node.optional, "?") + : "", + node.typeAnnotation ? ": " : "", + print("typeAnnotation"), + options.semi ? ifBreak(";") : "", + ]), + printDanglingComments(path, options), + options.bracketSpacing ? line : softline, + "}", + ], + { shouldBreak } + ); +} + +export { + printFlowMappedTypeProperty, + printTypeScriptMappedTypeModifier, + printTypescriptMappedType, +}; diff --git a/src/language-js/print/type-parameters.js b/src/language-js/print/type-parameters.js index 998e44eb3..eea098dd2 100644 --- a/src/language-js/print/type-parameters.js +++ b/src/language-js/print/type-parameters.js @@ -16,7 +16,6 @@ import { shouldPrintComma, getFunctionParameters, isObjectType, - getTypeScriptMappedTypeModifier, } from "../utils/index.js"; import createGroupIdMapper from "../../utils/create-group-id-mapper.js"; import { @@ -24,6 +23,7 @@ import { shouldHugType, } from "./type-annotation.js"; import { isArrowFunctionVariableDeclarator } from "./assignment.js"; +import { printTypeScriptMappedTypeModifier } from "./mapped-type.js"; const getTypeParametersGroupId = createGroupIdMapper("typeParameters"); @@ -119,7 +119,7 @@ function printTypeParameter(path, options, print) { if (parent.type === "TSMappedType") { if (parent.readonly) { parts.push( - getTypeScriptMappedTypeModifier(parent.readonly, "readonly"), + printTypeScriptMappedTypeModifier(parent.readonly, "readonly"), " " ); } diff --git a/src/language-js/print/typescript.js b/src/language-js/print/typescript.js index 5565ec850..0f5367df1 100644 --- a/src/language-js/print/typescript.js +++ b/src/language-js/print/typescript.js @@ -1,8 +1,5 @@ -import { printDanglingComments } from "../../main/comments/print.js"; -import hasNewlineInRange from "../../utils/has-newline-in-range.js"; import { join, - line, softline, group, indent, @@ -12,7 +9,6 @@ import { import UnexpectedNodeError from "../../utils/unexpected-node-error.js"; import { isStringLiteral, - getTypeScriptMappedTypeModifier, shouldPrintComma, isCallExpression, isMemberExpression, @@ -20,7 +16,7 @@ import { isObjectOrRecordExpression, } from "../utils/index.js"; import isTsKeywordType from "../utils/is-ts-keyword-type.js"; -import { locStart, locEnd } from "../loc.js"; +import { locStart } from "../loc.js"; import { printOptionalToken, @@ -58,6 +54,7 @@ import { } from "./type-annotation.js"; import { printEnumDeclaration, printEnumMember } from "./enum.js"; import { printImportKind } from "./module.js"; +import { printTypescriptMappedType } from "./mapped-type.js"; function printTypescript(path, options, print) { const { node } = path; @@ -239,32 +236,10 @@ function printTypescript(path, options, print) { case "TSTypeOperator": return [node.operator, " ", print("typeAnnotation")]; - case "TSMappedType": { - const shouldBreak = hasNewlineInRange( - options.originalText, - locStart(node), - locEnd(node) - ); - return group( - [ - "{", - indent([ - options.bracketSpacing ? line : softline, - print("typeParameter"), - node.optional - ? getTypeScriptMappedTypeModifier(node.optional, "?") - : "", - node.typeAnnotation ? ": " : "", - print("typeAnnotation"), - ifBreak(semi), - ]), - printDanglingComments(path, options), - options.bracketSpacing ? line : softline, - "}", - ], - { shouldBreak } - ); - } + + case "TSMappedType": + return printTypescriptMappedType(path, options, print); + case "TSMethodSignature": { const kind = node.kind && node.kind !== "method" ? `${node.kind} ` : ""; parts.push( diff --git a/src/language-js/traverse/visitor-keys.evaluate.js b/src/language-js/traverse/visitor-keys.evaluate.js index f81071bde..820f1e693 100644 --- a/src/language-js/traverse/visitor-keys.evaluate.js +++ b/src/language-js/traverse/visitor-keys.evaluate.js @@ -38,6 +38,13 @@ const additionalVisitorKeys = { ], DeclareEnum: flowVisitorKeys.EnumDeclaration, InferTypeAnnotation: ["typeParameter"], + KeyofTypeAnnotation: ["argument"], + ObjectTypeMappedTypeProperty: [ + "keyTparam", + "propType", + "sourceType", + "variance", + ], QualifiedTypeofIdentifier: ["id", "qualification"], TupleTypeAnnotation: ["elementTypes"], TupleTypeSpreadElement: ["label", "typeAnnotation"], diff --git a/src/language-js/utils/index.js b/src/language-js/utils/index.js index 8a59f76b9..4dc06fd12 100644 --- a/src/language-js/utils/index.js +++ b/src/language-js/utils/index.js @@ -485,19 +485,6 @@ function isSimpleTemplateLiteral(node) { }); } -/** - * @param {string} tokenNode - * @param {string} keyword - * @returns {string} - */ -function getTypeScriptMappedTypeModifier(tokenNode, keyword) { - if (tokenNode === "+" || tokenNode === "-") { - return tokenNode + keyword; - } - - return keyword; -} - /** * @param {string} text * @param {Node} node @@ -1131,7 +1118,6 @@ export { hasRestParameter, getLeftSide, getLeftSidePathName, - getTypeScriptMappedTypeModifier, hasLeadingOwnLineComment, hasNakedLeftSide, hasNode, diff --git a/tests/format/flow/mapped-types/__snapshots__/jsfmt.spec.js.snap b/tests/format/flow/mapped-types/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000..aedba5a0d --- /dev/null +++ b/tests/format/flow/mapped-types/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,253 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`comments.js [babel-flow] format 1`] = ` +"Unexpected token, expected "]" (3:7) + 1 | type Type = { + 2 | // comment +> 3 | +[T in number]: number; + | ^ + 4 | }; + 5 | + 6 | type Type = {" +`; + +exports[`comments.js [babel-ts] format 1`] = ` +"Unexpected token (3:3) + 1 | type Type = { + 2 | // comment +> 3 | +[T in number]: number; + | ^ + 4 | }; + 5 | + 6 | type Type = {" +`; + +exports[`comments.js [typescript] format 1`] = ` +"Property or signature expected. (3:3) + 1 | type Type = { + 2 | // comment +> 3 | +[T in number]: number; + | ^ + 4 | }; + 5 | + 6 | type Type = {" +`; + +exports[`comments.js format 1`] = ` +====================================options===================================== +parsers: ["flow", "typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +type Type = { + // comment + +[T in number]: number; +}; + +type Type = { + // comment1 + // comment2 + +[T in number]: number; +}; + +type Type = { + // comment + -[T in number]: number; +}; + +type Type = { + // comment + + [T in number]: number; +}; + +type Type = { + // comment + + [T in number]: number; +}; + +type Type = { + // comment + + [T in number]: number; +}; + +type Type = { + // comment + [T in number]: number; +}; + +type Type = { + + // foo + /* bar */ [T in number]: number; +}; + +=====================================output===================================== +type Type = { + // comment + +[T in number]: number, +}; + +type Type = { + // comment1 + // comment2 + +[T in number]: number, +}; + +type Type = { + // comment + -[T in number]: number, +}; + +type Type = { + // comment + +[T in number]: number, +}; + +type Type = { + // comment + +[T in number]: number, +}; + +type Type = { + // comment + +[T in number]: number, +}; + +type Type = { + // comment + [T in number]: number, +}; + +type Type = { + +[/* bar */ T in number]: number, // foo +}; + +================================================================================ +`; + +exports[`mapped-types.js [babel-flow] format 1`] = ` +"Unexpected token, expected "]" (3:21) + 1 | //@flow + 2 | +> 3 | type Mapped = {[key in keyof O]: O[key]}; + | ^ + 4 | type MappedLong = {[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number}; + 5 | type MappedWithVariance = {+[key in keyof O]: number}; + 6 | type MappedWithVarianceLong = {-[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number};" +`; + +exports[`mapped-types.js [babel-ts] format 1`] = ` +"Unexpected token (5:28) + 3 | type Mapped = {[key in keyof O]: O[key]}; + 4 | type MappedLong = {[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number}; +> 5 | type MappedWithVariance = {+[key in keyof O]: number}; + | ^ + 6 | type MappedWithVarianceLong = {-[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number}; + 7 | type MappedWithPlusOptional = {[key in keyof O]+?: number}; + 8 | type MappedWithMinusOptional = {[key in keyof O]-?: number};" +`; + +exports[`mapped-types.js [typescript] format 1`] = ` +"Property or signature expected. (5:28) + 3 | type Mapped = {[key in keyof O]: O[key]}; + 4 | type MappedLong = {[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number}; +> 5 | type MappedWithVariance = {+[key in keyof O]: number}; + | ^ + 6 | type MappedWithVarianceLong = {-[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number}; + 7 | type MappedWithPlusOptional = {[key in keyof O]+?: number}; + 8 | type MappedWithMinusOptional = {[key in keyof O]-?: number};" +`; + +exports[`mapped-types.js format 1`] = ` +====================================options===================================== +parsers: ["flow", "typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +//@flow + +type Mapped = {[key in keyof O]: O[key]}; +type MappedLong = {[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number}; +type MappedWithVariance = {+[key in keyof O]: number}; +type MappedWithVarianceLong = {-[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number}; +type MappedWithPlusOptional = {[key in keyof O]+?: number}; +type MappedWithMinusOptional = {[key in keyof O]-?: number}; +type MappedWithOptional = {[key in keyof O]?: number}; +type MappedWithPlusOptionalLong = {[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]+?: number}; +type MappedWithMinusOptionalLong = {[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]-?: number}; +type MappedWithOptionalLong = {[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]?: number}; +type MappedWithOptionalAndVariance = {+[key in keyof O]+?: number}; +type MappedWithOptionalAndVarianceLong = {+[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]+?: number}; +type MappedWithComments = {/*comment about variance */+[key /* comment about key name */ in /* comment before source type */ keyof O /* comment after source type */]/* comment about optionality */? /*comment before colon */:number /*comment about the prop type */}; + +=====================================output===================================== +//@flow + +type Mapped = { [key in keyof O]: O[key] }; +type MappedLong = { + [key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number, +}; +type MappedWithVariance = { +[key in keyof O]: number }; +type MappedWithVarianceLong = { + -[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number, +}; +type MappedWithPlusOptional = { [key in keyof O]+?: number }; +type MappedWithMinusOptional = { [key in keyof O]-?: number }; +type MappedWithOptional = { [key in keyof O]?: number }; +type MappedWithPlusOptionalLong = { + [key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]+?: number, +}; +type MappedWithMinusOptionalLong = { + [key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]-?: number, +}; +type MappedWithOptionalLong = { + [key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]?: number, +}; +type MappedWithOptionalAndVariance = { +[key in keyof O]+?: number }; +type MappedWithOptionalAndVarianceLong = { + +[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]+?: number, +}; +type MappedWithComments = { + /*comment about variance */ +[key /* comment about key name */ in /* comment before source type */ keyof O /* comment after source type */ /* comment about optionality */ /*comment before colon */]?: number /*comment about the prop type */, +}; + +================================================================================ +`; + +exports[`ts-compatibility.js [babel-flow] format 1`] = ` +"Unexpected token, expected "]" (3:19) + 1 | // Tests that on single-line mapped types Flow and TS make the same formatting decisions + 2 | +> 3 | type Test = {[key in T]: number}; + | ^ + 4 | type Test = {[key in keyof T]: number}; + 5 | type Test = {[otherKeyName in keyof T]: number}; + 6 | type Test = {[key in T]:number};" +`; + +exports[`ts-compatibility.js format 1`] = ` +====================================options===================================== +parsers: ["flow", "typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +// Tests that on single-line mapped types Flow and TS make the same formatting decisions + +type Test = {[key in T]: number}; +type Test = {[key in keyof T]: number}; +type Test = {[otherKeyName in keyof T]: number}; +type Test = {[key in T]:number}; +type Test = {[key in T]+?:number}; +type Test = {[key in T]-?: number}; + +=====================================output===================================== +// Tests that on single-line mapped types Flow and TS make the same formatting decisions + +type Test = { [key in T]: number }; +type Test = { [key in keyof T]: number }; +type Test = { [otherKeyName in keyof T]: number }; +type Test = { [key in T]: number }; +type Test = { [key in T]+?: number }; +type Test = { [key in T]-?: number }; + +================================================================================ +`; diff --git a/tests/format/flow/mapped-types/comments.js b/tests/format/flow/mapped-types/comments.js new file mode 100644 index 000000000..fd51657a4 --- /dev/null +++ b/tests/format/flow/mapped-types/comments.js @@ -0,0 +1,40 @@ +type Type = { + // comment + +[T in number]: number; +}; + +type Type = { + // comment1 + // comment2 + +[T in number]: number; +}; + +type Type = { + // comment + -[T in number]: number; +}; + +type Type = { + // comment + + [T in number]: number; +}; + +type Type = { + // comment + + [T in number]: number; +}; + +type Type = { + // comment + + [T in number]: number; +}; + +type Type = { + // comment + [T in number]: number; +}; + +type Type = { + + // foo + /* bar */ [T in number]: number; +}; diff --git a/tests/format/flow/mapped-types/jsfmt.spec.js b/tests/format/flow/mapped-types/jsfmt.spec.js new file mode 100644 index 000000000..066cf3d1f --- /dev/null +++ b/tests/format/flow/mapped-types/jsfmt.spec.js @@ -0,0 +1,7 @@ +run_spec(import.meta, ["flow", "typescript"], { + errors: { + "babel-flow": ["comments.js", "mapped-types.js", "ts-compatibility.js"], + "babel-ts": ["comments.js", "mapped-types.js"], + typescript: ["comments.js", "mapped-types.js"], + }, +}); diff --git a/tests/format/flow/mapped-types/mapped-types.js b/tests/format/flow/mapped-types/mapped-types.js new file mode 100644 index 000000000..5be422501 --- /dev/null +++ b/tests/format/flow/mapped-types/mapped-types.js @@ -0,0 +1,15 @@ +//@flow + +type Mapped = {[key in keyof O]: O[key]}; +type MappedLong = {[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number}; +type MappedWithVariance = {+[key in keyof O]: number}; +type MappedWithVarianceLong = {-[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]: number}; +type MappedWithPlusOptional = {[key in keyof O]+?: number}; +type MappedWithMinusOptional = {[key in keyof O]-?: number}; +type MappedWithOptional = {[key in keyof O]?: number}; +type MappedWithPlusOptionalLong = {[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]+?: number}; +type MappedWithMinusOptionalLong = {[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]-?: number}; +type MappedWithOptionalLong = {[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]?: number}; +type MappedWithOptionalAndVariance = {+[key in keyof O]+?: number}; +type MappedWithOptionalAndVarianceLong = {+[key in keyof AReallyLongNameThatShouldReallyMostDefinitelyCauseALineWrap]+?: number}; +type MappedWithComments = {/*comment about variance */+[key /* comment about key name */ in /* comment before source type */ keyof O /* comment after source type */]/* comment about optionality */? /*comment before colon */:number /*comment about the prop type */}; diff --git a/tests/format/flow/mapped-types/ts-compatibility.js b/tests/format/flow/mapped-types/ts-compatibility.js new file mode 100644 index 000000000..a9453b7c4 --- /dev/null +++ b/tests/format/flow/mapped-types/ts-compatibility.js @@ -0,0 +1,8 @@ +// Tests that on single-line mapped types Flow and TS make the same formatting decisions + +type Test = {[key in T]: number}; +type Test = {[key in keyof T]: number}; +type Test = {[otherKeyName in keyof T]: number}; +type Test = {[key in T]:number}; +type Test = {[key in T]+?:number}; +type Test = {[key in T]-?: number}; diff --git a/tests/unit/__snapshots__/visitor-keys.js.snap b/tests/unit/__snapshots__/visitor-keys.js.snap index fda608497..2652e35e0 100644 --- a/tests/unit/__snapshots__/visitor-keys.js.snap +++ b/tests/unit/__snapshots__/visitor-keys.js.snap @@ -446,6 +446,9 @@ exports[`visitor keys estree 1`] = ` "JsonRoot": [ "node", ], + "KeyofTypeAnnotation": [ + "argument", + ], "LabeledStatement": [ "label", "body", @@ -563,6 +566,12 @@ exports[`visitor keys estree 1`] = ` "static", "method", ], + "ObjectTypeMappedTypeProperty": [ + "keyTparam", + "propType", + "sourceType", + "variance", + ], "ObjectTypeProperty": [ "key", "value",