diff --git a/package.json b/package.json index 040efd6c..44b1618f 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "resolve": "1.5.0", "semver": "5.4.1", "string-width": "2.1.1", - "typescript": "2.7.0-insiders.20171214", + "typescript": "2.8.0-dev.20180222", "typescript-eslint-parser": "14.0.0", "unicode-regex": "1.0.1", "unified": "6.1.6" diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js index 7bddbc24..59d64ad2 100644 --- a/src/language-js/printer-estree.js +++ b/src/language-js/printer-estree.js @@ -172,6 +172,135 @@ function hasJsxIgnoreComment(path) { ); } +// The following is the shared logic for +// ternary operators, namely ConditionalExpression +// and TSConditionalType +function formatTernaryOperator(path, options, print, operatorOptions) { + const n = path.getValue(); + const parts = []; + const operatorOpts = Object.assign( + { + beforeParts: () => [""], + afterParts: () => [""], + shouldCheckJsx: true, + operatorName: "ConditionalExpression", + consequentNode: "consequent", + alternateNode: "alternate", + testNode: "test" + }, + operatorOptions || {} + ); + + // We print a ConditionalExpression in either "JSX mode" or "normal mode". + // See tests/jsx/conditional-expression.js for more info. + let jsxMode = false; + const parent = path.getParentNode(); + let forceNoIndent = parent.type === operatorOpts.operatorName; + + // Find the outermost non-ConditionalExpression parent, and the outermost + // ConditionalExpression parent. We'll use these to determine if we should + // print in JSX mode. + let currentParent; + let previousParent; + let i = 0; + do { + previousParent = currentParent || n; + currentParent = path.getParentNode(i); + i++; + } while (currentParent && currentParent.type === operatorOpts.operatorName); + const firstNonConditionalParent = currentParent || parent; + const lastConditionalParent = previousParent; + + if ( + (operatorOpts.shouldCheckJsx && isJSXNode(n[operatorOpts.testNode])) || + isJSXNode(n[operatorOpts.consequentNode]) || + isJSXNode(n[operatorOpts.alternateNode]) || + conditionalExpressionChainContainsJSX(lastConditionalParent) + ) { + jsxMode = true; + forceNoIndent = true; + + // Even though they don't need parens, we wrap (almost) everything in + // parens when using ?: within JSX, because the parens are analogous to + // curly braces in an if statement. + const wrap = doc => + concat([ + ifBreak("(", ""), + indent(concat([softline, doc])), + softline, + ifBreak(")", "") + ]); + + // The only things we don't wrap are: + // * Nested conditional expressions in alternates + // * null + const isNull = node => + node.type === "NullLiteral" || + (node.type === "Literal" && node.value === null); + + parts.push( + " ? ", + isNull(n[operatorOpts.consequentNode]) + ? path.call(print, operatorOpts.consequentNode) + : wrap(path.call(print, operatorOpts.consequentNode)), + " : ", + n[operatorOpts.alternateNode].type === operatorOpts.operatorName || + isNull(n[operatorOpts.alternateNode]) + ? path.call(print, operatorOpts.alternateNode) + : wrap(path.call(print, operatorOpts.alternateNode)) + ); + } else { + // normal mode + const part = concat([ + line, + "? ", + n[operatorOpts.consequentNode].type === operatorOpts.operatorName + ? ifBreak("", "(") + : "", + align(2, path.call(print, operatorOpts.consequentNode)), + n[operatorOpts.consequentNode].type === operatorOpts.operatorName + ? ifBreak("", ")") + : "", + line, + ": ", + align(2, path.call(print, operatorOpts.alternateNode)) + ]); + parts.push( + parent.type === operatorOpts.operatorName + ? options.useTabs + ? dedent(indent(part)) + : align(Math.max(0, options.tabWidth - 2), part) + : part + ); + } + + // In JSX mode, we want a whole chain of ConditionalExpressions to all + // break if any of them break. That means we should only group around the + // outer-most ConditionalExpression. + const maybeGroup = doc => + jsxMode + ? parent === firstNonConditionalParent ? group(doc) : doc + : group(doc); // Always group in normal mode. + + // Break the closing paren to keep the chain right after it: + // (a + // ? b + // : c + // ).call() + const breakClosingParen = + !jsxMode && parent.type === "MemberExpression" && !parent.computed; + + return maybeGroup( + concat( + [].concat( + operatorOpts.beforeParts(), + forceNoIndent ? concat(parts) : indent(concat(parts)), + operatorOpts.afterParts(breakClosingParen) + ) + ) + ); +} + function printPathNoParens(path, options, print, args) { const n = path.getValue(); const semi = options.semi ? ";" : ""; @@ -1222,109 +1351,11 @@ function printPathNoParens(path, options, print, args) { } return concat(parts); - case "ConditionalExpression": { - // We print a ConditionalExpression in either "JSX mode" or "normal mode". - // See tests/jsx/conditional-expression.js for more info. - let jsxMode = false; - const parent = path.getParentNode(); - let forceNoIndent = parent.type === "ConditionalExpression"; - - // Find the outermost non-ConditionalExpression parent, and the outermost - // ConditionalExpression parent. We'll use these to determine if we should - // print in JSX mode. - let currentParent; - let previousParent; - let i = 0; - do { - previousParent = currentParent || n; - currentParent = path.getParentNode(i); - i++; - } while (currentParent && currentParent.type === "ConditionalExpression"); - const firstNonConditionalParent = currentParent || parent; - const lastConditionalParent = previousParent; - - if ( - isJSXNode(n.test) || - isJSXNode(n.consequent) || - isJSXNode(n.alternate) || - conditionalExpressionChainContainsJSX(lastConditionalParent) - ) { - jsxMode = true; - forceNoIndent = true; - - // Even though they don't need parens, we wrap (almost) everything in - // parens when using ?: within JSX, because the parens are analogous to - // curly braces in an if statement. - const wrap = doc => - concat([ - ifBreak("(", ""), - indent(concat([softline, doc])), - softline, - ifBreak(")", "") - ]); - - // The only things we don't wrap are: - // * Nested conditional expressions in alternates - // * null - const isNull = node => - node.type === "NullLiteral" || - (node.type === "Literal" && node.value === null); - - parts.push( - " ? ", - isNull(n.consequent) - ? path.call(print, "consequent") - : wrap(path.call(print, "consequent")), - " : ", - n.alternate.type === "ConditionalExpression" || isNull(n.alternate) - ? path.call(print, "alternate") - : wrap(path.call(print, "alternate")) - ); - } else { - // normal mode - const part = concat([ - line, - "? ", - n.consequent.type === "ConditionalExpression" ? ifBreak("", "(") : "", - align(2, path.call(print, "consequent")), - n.consequent.type === "ConditionalExpression" ? ifBreak("", ")") : "", - line, - ": ", - align(2, path.call(print, "alternate")) - ]); - parts.push( - parent.type === "ConditionalExpression" - ? options.useTabs - ? dedent(indent(part)) - : align(Math.max(0, options.tabWidth - 2), part) - : part - ); - } - - // In JSX mode, we want a whole chain of ConditionalExpressions to all - // break if any of them break. That means we should only group around the - // outer-most ConditionalExpression. - const maybeGroup = doc => - jsxMode - ? parent === firstNonConditionalParent ? group(doc) : doc - : group(doc); // Always group in normal mode. - - // Break the closing paren to keep the chain right after it: - // (a - // ? b - // : c - // ).call() - const breakClosingParen = - !jsxMode && parent.type === "MemberExpression" && !parent.computed; - - return maybeGroup( - concat([ - path.call(print, "test"), - forceNoIndent ? concat(parts) : indent(concat(parts)), - breakClosingParen ? softline : "" - ]) - ); - } + case "ConditionalExpression": + return formatTernaryOperator(path, options, print, { + beforeParts: () => [path.call(print, "test")], + afterParts: breakClosingParen => [breakClosingParen ? softline : ""] + }); case "VariableDeclaration": { const printed = path.map(childPath => { return print(childPath); @@ -2872,6 +2903,25 @@ function printPathNoParens(path, options, print, args) { case "PrivateName": return concat(["#", path.call(print, "id")]); + case "TSConditionalType": + return formatTernaryOperator(path, options, print, { + beforeParts: () => [ + path.call(print, "checkType"), + " ", + "extends", + " ", + path.call(print, "extendsType") + ], + shouldCheckJsx: false, + operatorName: "TSConditionalType", + consequentNode: "trueType", + alternateNode: "falseType", + testNode: "checkType" + }); + + case "TSInferType": + return concat(["infer", " ", path.call(print, "typeParameter")]); + default: /* istanbul ignore next */ throw new Error("unknown type: " + JSON.stringify(n.type)); diff --git a/tests/typescript_conditional_types/__snapshots__/jsfmt.spec.js.snap b/tests/typescript_conditional_types/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 00000000..a06b23e6 --- /dev/null +++ b/tests/typescript_conditional_types/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`conditonal-types.ts 1`] = ` +export type DeepReadonly = T extends any[] ? DeepReadonlyArray : T extends object ? DeepReadonlyObject : T; + +type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]; + +interface DeepReadonlyArray extends ReadonlyArray> {} + +type DeepReadonlyObject = { + readonly [P in NonFunctionPropertyNames]: DeepReadonly; +}; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +export type DeepReadonly = T extends any[] + ? DeepReadonlyArray + : T extends object ? DeepReadonlyObject : T; + +type NonFunctionPropertyNames = { + [K in keyof T]: T[K] extends Function ? never : K +}[keyof T]; + +interface DeepReadonlyArray extends ReadonlyArray> {} + +type DeepReadonlyObject = { + readonly [P in NonFunctionPropertyNames]: DeepReadonly +}; + +`; + +exports[`infer-type.ts 1`] = ` +type TestReturnType any> = T extends (...args: any[]) => infer R ? R : any; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +type TestReturnType any> = T extends ( + ...args: any[] +) => infer R + ? R + : any; + +`; diff --git a/tests/typescript_conditional_types/conditonal-types.ts b/tests/typescript_conditional_types/conditonal-types.ts new file mode 100644 index 00000000..af1847f7 --- /dev/null +++ b/tests/typescript_conditional_types/conditonal-types.ts @@ -0,0 +1,9 @@ +export type DeepReadonly = T extends any[] ? DeepReadonlyArray : T extends object ? DeepReadonlyObject : T; + +type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]; + +interface DeepReadonlyArray extends ReadonlyArray> {} + +type DeepReadonlyObject = { + readonly [P in NonFunctionPropertyNames]: DeepReadonly; +}; diff --git a/tests/typescript_conditional_types/infer-type.ts b/tests/typescript_conditional_types/infer-type.ts new file mode 100644 index 00000000..ed8e81c8 --- /dev/null +++ b/tests/typescript_conditional_types/infer-type.ts @@ -0,0 +1 @@ +type TestReturnType any> = T extends (...args: any[]) => infer R ? R : any; diff --git a/tests/typescript_conditional_types/jsfmt.spec.js b/tests/typescript_conditional_types/jsfmt.spec.js new file mode 100644 index 00000000..2ea3bb6e --- /dev/null +++ b/tests/typescript_conditional_types/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, ["typescript"]); diff --git a/yarn.lock b/yarn.lock index 2393d2dd..565a1764 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4586,9 +4586,9 @@ typescript-eslint-parser@14.0.0: lodash.unescape "4.0.1" semver "5.5.0" -typescript@2.7.0-insiders.20171214: - version "2.7.0-insiders.20171214" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.0-insiders.20171214.tgz#841344ddae5f498a97c0435fcd12860480050e71" +typescript@2.8.0-dev.20180222: + version "2.8.0-dev.20180222" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.0-dev.20180222.tgz#50ee5fd5c76f2c9817e949803f946d74a559fc01" uglify-es@3.0.28: version "3.0.28"