Handle closure compiler type cast syntax correctly (#2484)

* Handle closure compiler type cast syntax correctly

Fixes https://github.com/prettier/prettier/issues/1445

* Move closure type cast detection to needParens in fast-path.js

* every => some and added additional check for leading comment
master
Yang Su 2017-07-24 18:21:25 -07:00 committed by Lucas Azzola
parent a666a29aa3
commit 26842e4d69
5 changed files with 91 additions and 9 deletions

View File

@ -281,7 +281,7 @@ function attach(comments, ast, text) {
) ||
handleObjectPropertyAssignment(enclosingNode, precedingNode, comment) ||
handleCommentInEmptyParens(text, enclosingNode, comment) ||
handleMethodNameComments(enclosingNode, precedingNode, comment) ||
handleMethodNameComments(text, enclosingNode, precedingNode, comment) ||
handleOnlyComments(enclosingNode, ast, comment, isLastComment)
) {
// We're good
@ -337,7 +337,8 @@ function breakTies(tiesToBreak, text) {
// Iterate backwards through tiesToBreak, examining the gaps
// between the tied comments. In order to qualify as leading, a
// comment must be separated from followingNode by an unbroken series of
// whitespace-only gaps (or other comments).
// gaps (or other comments). Gaps should only contain whitespace or open
// parentheses.
let indexOfFirstLeadingComment;
for (
indexOfFirstLeadingComment = tieCount;
@ -348,13 +349,14 @@ function breakTies(tiesToBreak, text) {
assert.strictEqual(comment.precedingNode, precedingNode);
assert.strictEqual(comment.followingNode, followingNode);
const gap = text.slice(locEnd(comment), gapEndPos);
if (/\S/.test(gap)) {
// The gap string contained something other than whitespace.
const gap = text.slice(locEnd(comment), gapEndPos).trim();
if (gap === "" || /^\(+$/.test(gap)) {
gapEndPos = locStart(comment);
} else {
// The gap string contained something other than whitespace or open
// parentheses.
break;
}
gapEndPos = locStart(comment);
}
tiesToBreak.forEach((comment, i) => {
@ -551,7 +553,7 @@ function handleObjectPropertyAssignment(enclosingNode, precedingNode, comment) {
return false;
}
function handleMethodNameComments(enclosingNode, precedingNode, comment) {
function handleMethodNameComments(text, enclosingNode, precedingNode, comment) {
// This is only needed for estree parsers (flow, typescript) to attach
// after a method name:
// obj = { fn /*comment*/() {} };
@ -561,7 +563,10 @@ function handleMethodNameComments(enclosingNode, precedingNode, comment) {
(enclosingNode.type === "Property" ||
enclosingNode.type === "MethodDefinition") &&
precedingNode.type === "Identifier" &&
enclosingNode.key === precedingNode
enclosingNode.key === precedingNode &&
// special Property case: { key: /*comment*/(value) };
// comment should be attached to value instead of key
getNextNonSpaceNonCommentCharacter(text, precedingNode) !== ":"
) {
addTrailingComment(precedingNode, comment);
return true;

View File

@ -151,6 +151,12 @@ FastPath.prototype.needsParens = function(options) {
return false;
}
// Closure compiler requires that type casted expressions to be surrounded by
// parentheses.
if (util.hasClosureCompilerTypeCastComment(options.originalText, node)) {
return true;
}
// Identifiers never need parentheses.
if (node.type === "Identifier") {
return false;

View File

@ -441,6 +441,21 @@ function isBlockComment(comment) {
return comment.type === "Block" || comment.type === "CommentBlock";
}
function hasClosureCompilerTypeCastComment(text, node) {
// https://github.com/google/closure-compiler/wiki/Annotating-Types#type-casts
// Syntax example: var x = /** @type {string} */ (fruit);
return (
node.comments &&
node.comments.some(
comment =>
comment.leading &&
isBlockComment(comment) &&
comment.value.match(/^\*\s*@type\s*{[^}]+}\s*$/) &&
getNextNonSpaceNonCommentCharacter(text, comment) === "("
)
);
}
function getAlignmentSize(value, tabWidth, startIndex) {
startIndex = startIndex || 0;
@ -484,5 +499,6 @@ module.exports = {
startsWithNoLookaheadToken,
hasBlockComments,
isBlockComment,
hasClosureCompilerTypeCastComment,
getAlignmentSize
};

View File

@ -107,6 +107,45 @@ React.render(
`;
exports[`closure-compiler-type-cast.js 1`] = `
// test to make sure comments are attached correctly
let inlineComment = /* some comment */ (
someReallyLongFunctionCall(withLots, ofArguments));
let object = {
key: /* some comment */ (someReallyLongFunctionCall(withLots, ofArguments))
};
// preserve parens only for type casts
let assignment = /** @type {string} */ (getValue());
functionCall(1 + /** @type {string} */ (value), /** @type {!Foo} */ ({}));
function returnValue() {
return /** @type {!Array.<string>} */ (['hello', 'you']);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// test to make sure comments are attached correctly
let inlineComment = /* some comment */ someReallyLongFunctionCall(
withLots,
ofArguments
);
let object = {
key: /* some comment */ someReallyLongFunctionCall(withLots, ofArguments)
};
// preserve parens only for type casts
let assignment = /** @type {string} */ (getValue());
functionCall(1 + /** @type {string} */ (value), /** @type {!Foo} */ ({}));
function returnValue() {
return /** @type {!Array.<string>} */ (["hello", "you"]);
}
`;
exports[`dangling.js 1`] = `
var x = {/* dangling */};
var x = {

View File

@ -0,0 +1,16 @@
// test to make sure comments are attached correctly
let inlineComment = /* some comment */ (
someReallyLongFunctionCall(withLots, ofArguments));
let object = {
key: /* some comment */ (someReallyLongFunctionCall(withLots, ofArguments))
};
// preserve parens only for type casts
let assignment = /** @type {string} */ (getValue());
functionCall(1 + /** @type {string} */ (value), /** @type {!Foo} */ ({}));
function returnValue() {
return /** @type {!Array.<string>} */ (['hello', 'you']);
}