diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js index b0e2c03d..53144ec5 100644 --- a/src/language-js/needs-parens.js +++ b/src/language-js/needs-parens.js @@ -4,6 +4,7 @@ const assert = require("assert"); const util = require("../common/util"); const comments = require("./comments"); +const { hasFlowShorthandAnnotationComment } = require("./utils"); function hasClosureCompilerTypeCastComment(text, path, locStart, locEnd) { // https://github.com/google/closure-compiler/wiki/Annotating-Types#type-casts @@ -74,6 +75,16 @@ function needsParens(path, options) { return true; } + if ( + // Preserve parens if we have a Flow annotation comment, unless we're using the Flow + // parser. The Flow parser turns Flow comments into type annotation nodes in its + // AST, which we handle separately. + options.parser !== "flow" && + hasFlowShorthandAnnotationComment(path.getValue()) + ) { + return true; + } + // Identifiers never need parentheses. if (node.type === "Identifier") { return false; diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js index 2be791f3..5342e305 100644 --- a/src/language-js/printer-estree.js +++ b/src/language-js/printer-estree.js @@ -34,6 +34,7 @@ const insertPragma = require("./pragma").insertPragma; const handleComments = require("./comments"); const pathNeedsParens = require("./needs-parens"); const preprocess = require("./preprocess"); +const { hasFlowShorthandAnnotationComment } = require("./utils"); const { builders: { @@ -166,6 +167,14 @@ function genericPrint(path, options, printPath, args) { parts.push(linesWithoutParens); if (needsParens) { + const node = path.getValue(); + if (hasFlowShorthandAnnotationComment(node)) { + parts.push(" /*"); + parts.push(node.trailingComments[0].value.trimLeft()); + parts.push("*/"); + node.trailingComments[0].printed = true; + } + parts.push(")"); } @@ -2836,14 +2845,36 @@ function printPathNoParens(path, options, print, args) { return group(concat(parts)); } - case "TypeCastExpression": + case "TypeCastExpression": { + const value = path.getValue(); + // Flow supports a comment syntax for specifying type annotations: https://flow.org/en/docs/types/comments/. + // Unfortunately, its parser doesn't differentiate between comment annotations and regular + // annotations when producing an AST. So to preserve parentheses around type casts that use + // the comment syntax, we need to hackily read the source itself to see if the code contains + // a type annotation comment. + // + // Note that we're able to use the normal whitespace regex here because the Flow parser has + // already deemed this AST node to be a type cast. Only the Babylon parser needs the + // non-line-break whitespace regex, which is why hasFlowShorthandAnnotationComment() is + // implemented differently. + const commentSyntax = + value && + value.typeAnnotation && + value.typeAnnotation.range && + options.originalText + .substring(value.typeAnnotation.range[0]) + .match(/^\/\*\s*:/); return concat([ "(", path.call(print, "expression"), + commentSyntax ? " /*" : "", ": ", path.call(print, "typeAnnotation"), + commentSyntax ? " */" : "", ")" ]); + } + case "TypeParameterDeclaration": case "TypeParameterInstantiation": case "TSTypeParameterDeclaration": @@ -5994,7 +6025,7 @@ function willPrintOwnComments(path) { const parent = path.getParentNode(); return ( - ((node && isJSXNode(node)) || + ((node && (isJSXNode(node) || hasFlowShorthandAnnotationComment(node))) || (parent && (parent.type === "JSXSpreadAttribute" || parent.type === "JSXSpreadChild" || diff --git a/src/language-js/utils.js b/src/language-js/utils.js new file mode 100644 index 00000000..11225546 --- /dev/null +++ b/src/language-js/utils.js @@ -0,0 +1,26 @@ +"use strict"; + +function hasFlowShorthandAnnotationComment(node) { + // https://flow.org/en/docs/types/comments/ + // Syntax example: const r = new (window.Request /*: Class */)(""); + + return ( + node.extra && + node.extra.parenthesized && + node.trailingComments && + // We match any whitespace except line terminators because + // Flow annotation comments cannot be split across lines. For example: + // + // (this /* + // : any */).foo = 5; + // + // is not picked up by Flow (see https://github.com/facebook/flow/issues/7050), so + // removing the newline would create a type annotation that the user did not intend + // to create. + node.trailingComments[0].value.match(/^(?:(?=.)\s)*:/) + ); +} + +module.exports = { + hasFlowShorthandAnnotationComment +}; diff --git a/tests/flow_comments/__snapshots__/jsfmt.spec.js.snap b/tests/flow_comments/__snapshots__/jsfmt.spec.js.snap index 8f45bdf4..6b3ebd34 100644 --- a/tests/flow_comments/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow_comments/__snapshots__/jsfmt.spec.js.snap @@ -85,3 +85,32 @@ type Props = { }; `; + +exports[`type_annotations.js - flow-verify 1`] = ` +new (window.Request/*: Class */)(""); + +(this/*: any */).foo = 5; + +(this/* : any */).foo = 5; + +// This next example illustrates that Prettier will not remove a line break +// and unintentionally create a type annotation that was not there before. +(this/* +: any */).foo = 5; + +const x = (input /*: string */) /*: string */ => {}; +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +new (window.Request /*: Class */)(""); + +(this /*: any */).foo = 5; + +(this /*: any */).foo = 5; + +// This next example illustrates that Prettier will not remove a line break +// and unintentionally create a type annotation that was not there before. +this /* +: any */.foo = 5; + +const x = (input /*: string */) /*: string */ => {}; + +`; diff --git a/tests/flow_comments/type_annotations.js b/tests/flow_comments/type_annotations.js new file mode 100644 index 00000000..0cafa44e --- /dev/null +++ b/tests/flow_comments/type_annotations.js @@ -0,0 +1,12 @@ +new (window.Request/*: Class */)(""); + +(this/*: any */).foo = 5; + +(this/* : any */).foo = 5; + +// This next example illustrates that Prettier will not remove a line break +// and unintentionally create a type annotation that was not there before. +(this/* +: any */).foo = 5; + +const x = (input /*: string */) /*: string */ => {};