Repository: prettier/prettier (JavaScript)
Date: 2019-03-07 05:49:47+00:00
Task type: Bug Fix

Issue:
JSDoc type cast parentheses is removed 
**Prettier 1.13.7**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAucAbAhgZ0wAgMIAWArlANY7AA6UOtOA9AFSPWUw6M4ACATnDER5RcwAEYQIqOOigBfHAHcC-ZTxwQ1UCOxjL8xMooCWqVDgLoAbnBzocPEjCMBbOK3aN67i5gBKjlzgACgBKCnc2ADMNHCDIYXYwA1IAcR4IIgAHdUicXSNMADoAfQBzdKzMMKooNjqYenocAHUbaFQATxwoODgAEzyIHCS4MHJdG0ijHkx2aDdamHq+ASFYiKXNpJJUiszCgoBJKCMndFRQnAAyK436pk4uGA7Mm2AAUVgeF4gjWHlPLFtmQ0hlMiFCqV+P5YIFCDtLgBeZF5AgFah0TFYkIAbg2sg2K0ENEi50wcDxiwJchAABoQBBMk5oJhkKB0Dx0goAAochCslBWX59OkgUQ8dBjfgAZUykr+pWQMAccHpf3JPBg3IlpWc6GQpNQ5PpACtMAAPABCEqlMGl6FcABk-nADWTVSA5TM4DxkGL0KIOqhoKLMjw-jBmkY+rpkAAOAAM9LDEHJzQlmT9YbgGusor4AEciNM4Nr0Lr9UhDcaQOTnEYlSr6ZgFVIAIpEbSuqvu+kwANRmMEZAAJj7EpMCrwEGcer9Wh6oqI5IAKgGBdW4LJZEA)
```sh
--parser babylon
```

JSDoc [`@type`](http://usejsdoc.org/tags-type.html) allows casting types of a reference but it requires a pair parentheses around the target reference. Prettier should not remove it.

**Input:**
```jsx
class Chunk {
    /**
	 * @returns {boolean} whether or not the Chunk will have a runtime
	 */
	hasRuntime() {
		for (const chunkGroup of this._groups) {
			// We only need to check the first one
			return (
				chunkGroup.isInitial() &&
				/** @type {Entrypoint} */ (chunkGroup).getRuntimeChunk() === this
            );
		}
		return false;
	}
}
```

**Output:**
```jsx
class Chunk {
  /**
   * @returns {boolean} whether or not the Chunk will have a runtime
   */
  hasRuntime() {
    for (const chunkGroup of this._groups) {
      // We only need to check the first one
      return (
        chunkGroup.isInitial() &&
        /** @type {Entrypoint} */ (chunkGroup.getRuntimeChunk() === this)
      );
    }
    return false;
  }
}

```


**Expected behavior:**

```js
class Chunk {
    /**
	 * @returns {boolean} whether or not the Chunk will have a runtime
	 */
	hasRuntime() {
		for (const chunkGroup of this._groups) {
			// We only need to check the first one
			return (
				chunkGroup.isInitial() &&
				/** @type {Entrypoint} */ (chunkGroup).getRuntimeChunk() === this
            );
		}
		return false;
	}
}
```


Issue discussion:
Looks like it's caused by the outer parens:

**Prettier 1.13.7**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuc0DOMAEAPTBeTAegCpjMABGATwAc5NgANAX02MMwAoBDASgDoARp1748BMAG4AOlEhQMOfFxJlKtek1bsufISLETekkABoQEGjACW6ZKG4AnRxADuABScI0yENwBuENYAJmYggo7cYADWcDAAyjRR1lAA5sgwjgCucOYpaHCOMO6RqQC23MgAZtwANgXmAFZo2ABCkTFx8dxlcAAyKXDVdQ0gSY4Fjr6C3IJUtdBhNI4pMADqITAAFsgAHAAM5ssQBWuRNL7LcJP+Q+aOcACOWdYPJdzllUg19bkgBWVrBlsn80ClUrU4ABFLIQeDDX7mGCzDbBbbIABMSMi1lq4IAwhAyhVfFBoHcQFkCgAVWY+b4jODMZhAA)
```sh
--parser babylon
```

**Input:**
```jsx
const x = /** @type {X} */ (a).b() === c;
const x = (/** @type {X} */ (a).b() === c);
```

**Output:**
```jsx
const x = /** @type {X} */ (a).b() === c;
const x = /** @type {X} */ (a.b() === c);

```
This also happens even with only a single set of parens.

**Prettier 1.15.1**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuc0DOMAEATOAzOAJ0Lm0wF5MB6AKhswAEYBPABzk2GExLQgBsAbhwAUASiSZBEAJbYA3DzgArOGCwi4kgIZRmEqbLIBfY5hpVMI4MbEgANCAisYM9MlDbiEAO4AFLwQ0ZBBtaTkHEAAjQm0wAGs4GABlVjiZKABzZBhCAFc4RwALGABbfgB1Ipl4NDSwOGSgmplBGuYQsDRgxwy0Ihg-WMzS7WQ8bX5+x2U0AA8AIViEpOTtUrgAGQy4ccnpkFm55IzM-jgARTyIeD2pwpA0wn7CEKjtKOZ+aEjWQgyYBU5DAisgABwABkcfwg-QqsVYIT+cBewkiJAAjnkZCQhtoRmMkBN7o5+qUZDl8g80KdzlcbrsifsHjAPkDsCDkAAmRy5bQyfinADCEFKoxCUGgu0ceX6ABUPsEmfdTEA)
```sh
--parser babylon
```

**Input:**
```jsx
const deferred = /** @type {{ resolve (): void; reject (e: any): void }} */ ({})
```

**Output:**
```jsx
const deferred = /** @type {{ resolve (): void; reject (e: any): void }} */ {};

```
The AMP project is considering the use of `prettier` with our closure-compiled JS runtime. During today's design review (https://github.com/ampproject/amphtml/issues/20844#issuecomment-470288213), we encountered a version of this issue, which results in incorrect JSdoc typecasts for closure-compiler:

**Playground link:** [here](https://prettier.io/playground/#N4Igxg9gdgLgprEAuc0DOMAEATAhjXTAXkwHoAqczAARgE8AHOTYAQgCk1oB5AIwCs4YGAF9M5UpgAUAHSiZMDXACc0cTtCkVytRs2AZlASygBzMROmm4MACL5cUuADcEMAJTuAdGgCuvQykAFk93AG4QABoQCAYYI3RkUBVlCAB3AAUVBDRkEFxnCCNsKJBeZVwwAGsbAGUlMBNTZBhlXzhogAsYAFsAGwB1TqN4NAa4WpyRo2cRujywNFzokzVlGAyK0x7cZAAzXD61aP40AA8AIQrqutweuAAZEzh9w+OQU7Papr64AEVfBB4K8jh0QEpVHBlHleLheHQ+tBSgxjLABsUYJ1kAAOAAM0RREDUAwqDDyKLga1cpWUcAAjr4jLTNrhtrskAdQdE1D0jC02mC0D9-oDgRy3mCCLx0dhMcgAEzRVq4Ix9JoAYQgPR2eSg0Be0V8agAKnDcuLQSIREA)

**Original code:**
```js
const data = /** @type {!JsonObject} */ (
  parseJson(/**@type {string} */ (getData(event)).substr(4)));
```

**Modified code:**
```js
const data = /** @type {!JsonObject} */ (parseJson(
  /**@type {string} */ (getData(event).substr(4))
));
```
Notice how earlier, `(getData(event))` was being cast to `string`, but now, the cast is being applied to `(getData(event).substr(4))`.

/cc @vjeux @cramforce
Interestingly, if it’s not within a function, it works as expected. So we must have some kind of special casing for it that’s not always being ran. I’m on the phone so can’t investigate more right now but that’s promising, we probably just need to fix the fact that it doesn’t always run.

**Prettier 1.16.4**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAucBDANugFAegFR4ACMAngA5wAEwAzjAE4CWUA5gL6V46VYtwwARVDFRY4ANwQwAlNIB0NAK4AjOvSwAWWQG4AOlH34ipCtTXN2nbr35CRYybFkKVazdJAAaEBDIxG0DTIoKj09BAA7gAKoQhBKKjiEIwAJl4gyvSoYADW-ADKZNkWyAyKcN4AFjAAtugA6pWM8DRFYHD5cc2M4s0kyOA0Qd7MNHD0MFFZLDWoyABmGGPeAFY0AB4AQlm5Bag1cAAyzHALSxUga+v5FuhwAIqKEPBn6MsgRfRj9APKqMokdDQdJkJiweqpGCVZAADgADN5QRAxvUsmQBqC4N9JOl6HAAI6KRh4qaoGZzJCLN4XMY1Rilejlbw0W4PJ4vSnnbwiZQQlJQ5AAJm5WUY6AsAGEIDVZgMoNBTt5FGMACr-eJUsZsNhAA)
```sh
--parser babylon
```

**Input:**
```jsx
call(/**@type {string} */ (getData(event)).substr(4));

/**@type {string} */ (getData(event)).substr(4)
```

**Output:**
```jsx
call(/**@type {string} */ (getData(event).substr(4)));

/**@type {string} */ (getData(event)).substr(4);

```

Solution patch:
diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index 7b5a8f5be608..a89a80824180 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -41,3 +41,22 @@ Examples:
   ```
 
 -->
+
+- JavaScript: Fix closure compiler typecasts ([#5947] by [@jridgewell])
+
+  If a closing parenthesis follows after a typecast in an inner expression, the typecast would wrap everything to the that following parenthesis.
+
+  <!-- prettier-ignore -->
+  ```js
+  // Input
+  test(/** @type {!Array} */(arrOrString).length);
+  test(/** @type {!Array} */((arrOrString)).length + 1);
+
+  // Output (Prettier stable)
+  test(/** @type {!Array} */ (arrOrString.length));
+  test(/** @type {!Array} */ (arrOrString.length + 1));
+
+  // Output (Prettier master)
+  test(/** @type {!Array} */ (arrOrString).length);
+  test(/** @type {!Array} */ (arrOrString).length + 1);
+  ```
diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js
index 4ad67228afa9..51135fcae3e5 100644
--- a/src/language-js/needs-parens.js
+++ b/src/language-js/needs-parens.js
@@ -6,23 +6,21 @@ const util = require("../common/util");
 const comments = require("./comments");
 const { hasFlowShorthandAnnotationComment } = require("./utils");
 
-function hasClosureCompilerTypeCastComment(text, path, locStart, locEnd) {
+function hasClosureCompilerTypeCastComment(text, path) {
   // https://github.com/google/closure-compiler/wiki/Annotating-Types#type-casts
   // Syntax example: var x = /** @type {string} */ (fruit);
 
   const n = path.getValue();
 
   return (
-    util.getNextNonSpaceNonCommentCharacter(text, n, locEnd) === ")" &&
+    isParenthesized(n) &&
     (hasTypeCastComment(n) || hasAncestorTypeCastComment(0))
   );
 
   // for sub-item: /** @type {array} */ (numberOrString).map(x => x);
   function hasAncestorTypeCastComment(index) {
     const ancestor = path.getParentNode(index);
-    return ancestor &&
-      util.getNextNonSpaceNonCommentCharacter(text, ancestor, locEnd) !== ")" &&
-      /^[\s(]*$/.test(text.slice(locStart(ancestor), locStart(n)))
+    return ancestor && !isParenthesized(ancestor)
       ? hasTypeCastComment(ancestor) || hasAncestorTypeCastComment(index + 1)
       : false;
   }
@@ -34,12 +32,18 @@ function hasClosureCompilerTypeCastComment(text, path, locStart, locEnd) {
         comment =>
           comment.leading &&
           comments.isBlockComment(comment) &&
-          isTypeCastComment(comment.value) &&
-          util.getNextNonSpaceNonCommentCharacter(text, comment, locEnd) === "("
+          isTypeCastComment(comment.value)
       )
     );
   }
 
+  function isParenthesized(node) {
+    // Closure typecast comments only really make sense when _not_ using
+    // typescript or flow parsers, so we take advantage of the babel parser's
+    // parenthesized expressions.
+    return node.extra && node.extra.parenthesized;
+  }
+
   function isTypeCastComment(comment) {
     const trimmed = comment.trim();
     if (!/^\*\s*@type\s*\{[^]+\}$/.test(trimmed)) {
@@ -100,14 +104,7 @@ function needsParens(path, options) {
 
   // Closure compiler requires that type casted expressions to be surrounded by
   // parentheses.
-  if (
-    hasClosureCompilerTypeCastComment(
-      options.originalText,
-      path,
-      options.locStart,
-      options.locEnd
-    )
-  ) {
+  if (hasClosureCompilerTypeCastComment(options.originalText, path)) {
     return true;
   }
 


Test patch:
diff --git a/tests/comments/__snapshots__/jsfmt.spec.js.snap b/tests/comments/__snapshots__/jsfmt.spec.js.snap
index 9198e1dc3efc..1cc4bce165fe 100644
--- a/tests/comments/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/comments/__snapshots__/jsfmt.spec.js.snap
@@ -323,114 +323,6 @@ React.render(
 ================================================================================
 `;
 
-exports[`closure-compiler-type-cast.js 1`] = `
-====================================options=====================================
-parsers: ["flow", "babel"]
-printWidth: 80
-                                                                                | printWidth
-=====================================input======================================
-// 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());
-let value = /** @type {string} */ (this.members[0]).functionCall();
-
-functionCall(1 + /** @type {string} */ (value), /** @type {!Foo} */ ({}));
-
-function returnValue() {
-  return /** @type {!Array.<string>} */ (['hello', 'you']);
-}
-
-var newArray = /** @type {array} */ (numberOrString).map(x => x);
-var newArray = /** @type {array} */ ((numberOrString)).map(x => x);
-var newArray = /** @type {array} */ ((numberOrString).map(x => x));
-
-
-const data = functionCall(
-  arg1,
-  arg2,
-  /** @type {{height: number, width: number}} */ (arg3));
-
-// Invalid type casts
-const v = /** @type {} */ (value);
-const v = /** @type {}} */ (value);
-const v = /** @type } */ (value);
-const v = /** @type { */ (value);
-const v = /** @type {{} */ (value);
-
-const style = /** @type {{
-  width: number,
-  height: number,
-  marginTop: number,
-  marginLeft: number,
-  marginRight: number,
-  marginBottom: number,
-}} */ ({
-  width,
-  height,
-  ...margins,
-});
-
-=====================================output=====================================
-// 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());
-let value = /** @type {string} */ (this.members[0]).functionCall();
-
-functionCall(1 + /** @type {string} */ (value), /** @type {!Foo} */ ({}));
-
-function returnValue() {
-  return /** @type {!Array.<string>} */ (["hello", "you"]);
-}
-
-var newArray = /** @type {array} */ (numberOrString).map(x => x);
-var newArray = /** @type {array} */ (numberOrString).map(x => x);
-var newArray = /** @type {array} */ (numberOrString.map(x => x));
-
-const data = functionCall(
-  arg1,
-  arg2,
-  /** @type {{height: number, width: number}} */ (arg3)
-);
-
-// Invalid type casts
-const v = /** @type {} */ value;
-const v = /** @type {}} */ value;
-const v = /** @type } */ value;
-const v = /** @type { */ value;
-const v = /** @type {{} */ value;
-
-const style = /** @type {{
-  width: number,
-  height: number,
-  marginTop: number,
-  marginLeft: number,
-  marginRight: number,
-  marginBottom: number,
-}} */ ({
-  width,
-  height,
-  ...margins
-});
-
-================================================================================
-`;
-
 exports[`dangling.js 1`] = `
 ====================================options=====================================
 parsers: ["flow", "babel"]
diff --git a/tests/comments_closure_typecast/__snapshots__/jsfmt.spec.js.snap b/tests/comments_closure_typecast/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..a2549b7b2306
--- /dev/null
+++ b/tests/comments_closure_typecast/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,132 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`closure-compiler-type-cast.js 1`] = `
+====================================options=====================================
+parsers: ["babel"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+// 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());
+let value = /** @type {string} */ (this.members[0]).functionCall();
+
+functionCall(1 + /** @type {string} */ (value), /** @type {!Foo} */ ({}));
+
+function returnValue() {
+  return /** @type {!Array.<string>} */ (['hello', 'you']);
+}
+
+// Only numberOrString is typecast
+var newArray = /** @type {array} */ (numberOrString).map(x => x);
+var newArray = /** @type {array} */ ((numberOrString)).map(x => x);
+var newArray = test(/** @type {array} */ (numberOrString).map(x => x));
+var newArray = test(/** @type {array} */ ((numberOrString)).map(x => x));
+
+// The numberOrString.map CallExpression is typecast
+var newArray = /** @type {array} */ (numberOrString.map(x => x));
+var newArray = /** @type {array} */ ((numberOrString).map(x => x));
+var newArray = test(/** @type {array} */ (numberOrString.map(x => x)));
+var newArray = test(/** @type {array} */ ((numberOrString).map(x => x)));
+
+test(/** @type {number} */(num) + 1);
+test(/** @type {!Array} */(arrOrString).length + 1);
+test(/** @type {!Array} */((arrOrString)).length + 1);
+
+const data = functionCall(
+  arg1,
+  arg2,
+  /** @type {{height: number, width: number}} */ (arg3));
+
+// Invalid type casts
+const v = /** @type {} */ (value);
+const v = /** @type {}} */ (value);
+const v = /** @type } */ (value);
+const v = /** @type { */ (value);
+const v = /** @type {{} */ (value);
+
+const style = /** @type {{
+  width: number,
+  height: number,
+  marginTop: number,
+  marginLeft: number,
+  marginRight: number,
+  marginBottom: number,
+}} */ ({
+  width,
+  height,
+  ...margins,
+});
+
+=====================================output=====================================
+// 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());
+let value = /** @type {string} */ (this.members[0]).functionCall();
+
+functionCall(1 + /** @type {string} */ (value), /** @type {!Foo} */ ({}));
+
+function returnValue() {
+  return /** @type {!Array.<string>} */ (["hello", "you"]);
+}
+
+// Only numberOrString is typecast
+var newArray = /** @type {array} */ (numberOrString).map(x => x);
+var newArray = /** @type {array} */ (numberOrString).map(x => x);
+var newArray = test(/** @type {array} */ (numberOrString).map(x => x));
+var newArray = test(/** @type {array} */ (numberOrString).map(x => x));
+
+// The numberOrString.map CallExpression is typecast
+var newArray = /** @type {array} */ (numberOrString.map(x => x));
+var newArray = /** @type {array} */ (numberOrString.map(x => x));
+var newArray = test(/** @type {array} */ (numberOrString.map(x => x)));
+var newArray = test(/** @type {array} */ (numberOrString.map(x => x)));
+
+test(/** @type {number} */ (num) + 1);
+test(/** @type {!Array} */ (arrOrString).length + 1);
+test(/** @type {!Array} */ (arrOrString).length + 1);
+
+const data = functionCall(
+  arg1,
+  arg2,
+  /** @type {{height: number, width: number}} */ (arg3)
+);
+
+// Invalid type casts
+const v = /** @type {} */ value;
+const v = /** @type {}} */ value;
+const v = /** @type } */ value;
+const v = /** @type { */ value;
+const v = /** @type {{} */ value;
+
+const style = /** @type {{
+  width: number,
+  height: number,
+  marginTop: number,
+  marginLeft: number,
+  marginRight: number,
+  marginBottom: number,
+}} */ ({
+  width,
+  height,
+  ...margins
+});
+
+================================================================================
+`;
diff --git a/tests/comments/closure-compiler-type-cast.js b/tests/comments_closure_typecast/closure-compiler-type-cast.js
similarity index 68%
rename from tests/comments/closure-compiler-type-cast.js
rename to tests/comments_closure_typecast/closure-compiler-type-cast.js
index 8fce7d6f21c7..52ed42dd0f0d 100644
--- a/tests/comments/closure-compiler-type-cast.js
+++ b/tests/comments_closure_typecast/closure-compiler-type-cast.js
@@ -16,10 +16,21 @@ function returnValue() {
   return /** @type {!Array.<string>} */ (['hello', 'you']);
 }
 
+// Only numberOrString is typecast
 var newArray = /** @type {array} */ (numberOrString).map(x => x);
 var newArray = /** @type {array} */ ((numberOrString)).map(x => x);
+var newArray = test(/** @type {array} */ (numberOrString).map(x => x));
+var newArray = test(/** @type {array} */ ((numberOrString)).map(x => x));
+
+// The numberOrString.map CallExpression is typecast
+var newArray = /** @type {array} */ (numberOrString.map(x => x));
 var newArray = /** @type {array} */ ((numberOrString).map(x => x));
+var newArray = test(/** @type {array} */ (numberOrString.map(x => x)));
+var newArray = test(/** @type {array} */ ((numberOrString).map(x => x)));
 
+test(/** @type {number} */(num) + 1);
+test(/** @type {!Array} */(arrOrString).length + 1);
+test(/** @type {!Array} */((arrOrString)).length + 1);
 
 const data = functionCall(
   arg1,
diff --git a/tests/comments_closure_typecast/jsfmt.spec.js b/tests/comments_closure_typecast/jsfmt.spec.js
new file mode 100644
index 000000000000..8382eddeb1db
--- /dev/null
+++ b/tests/comments_closure_typecast/jsfmt.spec.js
@@ -0,0 +1,1 @@
+run_spec(__dirname, ["babel"]);