Preserve parens around expressions in default export declaration that start with function or class (#6133)

master
Lucas Duailibe 2019-05-20 09:31:38 -03:00 committed by GitHub
parent e222562b58
commit 7e47b4ea26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 242 additions and 69 deletions

View File

@ -304,6 +304,27 @@ type G = keyof T[];
type G = (keyof T)[];
```
### JavaScript: Keep parenthesis around functions and classes in `export default` declarations ([#6133] by [@duailibe])
Prettier was removing parenthesis from some expressions in `export default`, but if those are complex expressions that start with `function` or `class`, the parenthesis are required.
See below some practical examples, including one affecting TypeScript.
<!-- prettier-ignore -->
```ts
// Input
export default (function log() {}).toString();
export default (function log() {} as typeof console.log);
// Output (Prettier stable)
export default function log() {}.toString();
export default function log() {} as typeof console.log; // syntax error
// Output (Prettier master)
export default (function log() {}).toString();
export default (function log() {} as typeof console.log);
```
### TypeScript: Keep parentheses around a function called with non-null assertion. ([6136] by [@sosukesuzuki])
Previously, Prettier removes necessary parentheses around a call expression with non-null assertion. It happens when it's return value is called as function.
@ -336,6 +357,7 @@ const b = new (c()!)();
[#6129]: https://github.com/prettier/prettier/pull/6129
[#6130]: https://github.com/prettier/prettier/pull/6130
[#6131]: https://github.com/prettier/prettier/pull/6131
[#6133]: https://github.com/prettier/prettier/pull/6133
[#6136]: https://github.com/prettier/prettier/pull/6136
[@belochub]: https://github.com/belochub
[@brainkim]: https://github.com/brainkim

View File

@ -4,7 +4,11 @@ const assert = require("assert");
const util = require("../common/util");
const comments = require("./comments");
const { hasFlowShorthandAnnotationComment } = require("./utils");
const {
getLeftSidePathName,
hasNakedLeftSide,
hasFlowShorthandAnnotationComment
} = require("./utils");
function hasClosureCompilerTypeCastComment(text, path) {
// https://github.com/google/closure-compiler/wiki/Annotating-Types#type-casts
@ -155,6 +159,13 @@ function needsParens(path, options) {
return true;
}
// `export default function` or `export default class` can't be followed by
// anything after. So an expression like `export default (function(){}).toString()`
// needs to be followed by a parentheses
if (parent.type === "ExportDefaultDeclaration") {
return shouldWrapFunctionForExportDefault(path, options);
}
if (parent.type === "Decorator" && parent.expression === node) {
let hasCallExpression = false;
let hasMemberExpression = false;
@ -311,6 +322,7 @@ function needsParens(path, options) {
case "ClassExpression":
case "ClassDeclaration":
return name === "superClass" && parent.superClass === node;
case "TSTypeAssertion":
case "TaggedTemplateExpression":
case "UnaryExpression":
@ -622,8 +634,6 @@ function needsParens(path, options) {
return name === "callee"; // Not strictly necessary, but it's clearer to the reader if IIFEs are wrapped in parentheses.
case "TaggedTemplateExpression":
return true; // This is basically a kind of IIFE.
case "ExportDefaultDeclaration":
return true;
default:
return false;
}
@ -658,8 +668,6 @@ function needsParens(path, options) {
case "ClassExpression":
switch (parent.type) {
case "ExportDefaultDeclaration":
return true;
case "NewExpression":
return name === "callee" && parent.callee === node;
default:
@ -839,4 +847,33 @@ function isFollowedByRightBracket(path) {
return false;
}
function shouldWrapFunctionForExportDefault(path, options) {
const node = path.getValue();
const parent = path.getParentNode();
if (node.type === "FunctionExpression" || node.type === "ClassExpression") {
return (
parent.type === "ExportDefaultDeclaration" ||
// in some cases the function is already wrapped
// (e.g. `export default (function() {})();`)
// in this case we don't need to add extra parens
!needsParens(path, options)
);
}
if (
!hasNakedLeftSide(node) ||
(parent.type !== "ExportDefaultDeclaration" && needsParens(path, options))
) {
return false;
}
return path.call.apply(
path,
[
childPath => shouldWrapFunctionForExportDefault(childPath, options)
].concat(getLeftSidePathName(path, node))
);
}
module.exports = needsParens;

View File

@ -41,6 +41,9 @@ const {
} = require("./html-binding");
const preprocess = require("./preprocess");
const {
getLeftSide,
getLeftSidePathName,
hasNakedLeftSide,
hasNode,
hasFlowAnnotationComment,
hasFlowShorthandAnnotationComment
@ -6002,74 +6005,12 @@ function hasLeadingOwnLineComment(text, node, options) {
return res;
}
function hasNakedLeftSide(node) {
return (
node.type === "AssignmentExpression" ||
node.type === "BinaryExpression" ||
node.type === "LogicalExpression" ||
node.type === "NGPipeExpression" ||
node.type === "ConditionalExpression" ||
node.type === "CallExpression" ||
node.type === "OptionalCallExpression" ||
node.type === "MemberExpression" ||
node.type === "OptionalMemberExpression" ||
node.type === "SequenceExpression" ||
node.type === "TaggedTemplateExpression" ||
node.type === "BindExpression" ||
(node.type === "UpdateExpression" && !node.prefix) ||
node.type === "TSNonNullExpression"
);
}
function isFlowAnnotationComment(text, typeAnnotation, options) {
const start = options.locStart(typeAnnotation);
const end = skipWhitespace(text, options.locEnd(typeAnnotation));
return text.substr(start, 2) === "/*" && text.substr(end, 2) === "*/";
}
function getLeftSide(node) {
if (node.expressions) {
return node.expressions[0];
}
return (
node.left ||
node.test ||
node.callee ||
node.object ||
node.tag ||
node.argument ||
node.expression
);
}
function getLeftSidePathName(path, node) {
if (node.expressions) {
return ["expressions", 0];
}
if (node.left) {
return ["left"];
}
if (node.test) {
return ["test"];
}
if (node.object) {
return ["object"];
}
if (node.callee) {
return ["callee"];
}
if (node.tag) {
return ["tag"];
}
if (node.argument) {
return ["argument"];
}
if (node.expression) {
return ["expression"];
}
throw new Error("Unexpected node has no left side", node);
}
function exprNeedsASIProtection(path, options) {
const node = path.getValue();

View File

@ -44,7 +44,73 @@ function hasNode(node, fn) {
: Object.keys(node).some(key => hasNode(node[key], fn));
}
function hasNakedLeftSide(node) {
return (
node.type === "AssignmentExpression" ||
node.type === "BinaryExpression" ||
node.type === "LogicalExpression" ||
node.type === "NGPipeExpression" ||
node.type === "ConditionalExpression" ||
node.type === "CallExpression" ||
node.type === "OptionalCallExpression" ||
node.type === "MemberExpression" ||
node.type === "OptionalMemberExpression" ||
node.type === "SequenceExpression" ||
node.type === "TaggedTemplateExpression" ||
node.type === "BindExpression" ||
(node.type === "UpdateExpression" && !node.prefix) ||
node.type === "TSAsExpression" ||
node.type === "TSNonNullExpression"
);
}
function getLeftSide(node) {
if (node.expressions) {
return node.expressions[0];
}
return (
node.left ||
node.test ||
node.callee ||
node.object ||
node.tag ||
node.argument ||
node.expression
);
}
function getLeftSidePathName(path, node) {
if (node.expressions) {
return ["expressions", 0];
}
if (node.left) {
return ["left"];
}
if (node.test) {
return ["test"];
}
if (node.object) {
return ["object"];
}
if (node.callee) {
return ["callee"];
}
if (node.tag) {
return ["tag"];
}
if (node.argument) {
return ["argument"];
}
if (node.expression) {
return ["expression"];
}
throw new Error("Unexpected node has no left side", node);
}
module.exports = {
getLeftSide,
getLeftSidePathName,
hasNakedLeftSide,
hasNode,
hasFlowShorthandAnnotationComment,
hasFlowAnnotationComment

View File

@ -1,8 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`binary_and_template.js 1`] = `
====================================options=====================================
parsers: ["babel", "flow", "typescript"]
printWidth: 80
| printWidth
=====================================input======================================
export default (function() {} + foo)\`\`;
=====================================output=====================================
export default (function() {} + foo)\`\`;
================================================================================
`;
exports[`body.js 1`] = `
====================================options=====================================
parsers: ["flow", "typescript"]
parsers: ["babel", "flow", "typescript"]
printWidth: 80
| printWidth
=====================================input======================================
@ -13,3 +27,59 @@ export default (class {}[1] = 1);
================================================================================
`;
exports[`class_instance.js 1`] = `
====================================options=====================================
parsers: ["babel", "flow", "typescript"]
printWidth: 80
| printWidth
=====================================input======================================
export default (class {}.getInstance());
=====================================output=====================================
export default (class {}.getInstance());
================================================================================
`;
exports[`function_in_template.js 1`] = `
====================================options=====================================
parsers: ["babel", "flow", "typescript"]
printWidth: 80
| printWidth
=====================================input======================================
export default (function templ() {})\`foo\`;
=====================================output=====================================
export default (function templ() {})\`foo\`;
================================================================================
`;
exports[`function_tostring.js 1`] = `
====================================options=====================================
parsers: ["babel", "flow", "typescript"]
printWidth: 80
| printWidth
=====================================input======================================
export default (function() {}).toString();
=====================================output=====================================
export default (function() {}.toString());
================================================================================
`;
exports[`iife.js 1`] = `
====================================options=====================================
parsers: ["babel", "flow", "typescript"]
printWidth: 80
| printWidth
=====================================input======================================
export default (function foo() {})();
=====================================output=====================================
export default (function foo() {})();
================================================================================
`;

View File

@ -0,0 +1 @@
export default (function() {} + foo)``;

View File

@ -0,0 +1 @@
export default (class {}.getInstance());

View File

@ -0,0 +1 @@
export default (function templ() {})`foo`;

View File

@ -0,0 +1 @@
export default (function() {}).toString();

View File

@ -0,0 +1 @@
export default (function foo() {})();

View File

@ -1 +1 @@
run_spec(__dirname, ["flow", "typescript"]);
run_spec(__dirname, ["babel", "flow", "typescript"]);

View File

@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`function_as.ts 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
export default (function log(){} as typeof console.log);
=====================================output=====================================
export default (function log() {} as typeof console.log);
================================================================================
`;

View File

@ -0,0 +1 @@
export default (function log(){} as typeof console.log);

View File

@ -0,0 +1 @@
run_spec(__dirname, ["typescript"]);

View File

@ -75,3 +75,17 @@ const state = JSON.stringify({
================================================================================
`;
exports[`export_default_as.ts 1`] = `
====================================options=====================================
parsers: ["typescript"]
printWidth: 80
| printWidth
=====================================input======================================
export default (function log() {} as typeof console.log)
=====================================output=====================================
export default (function log() {} as typeof console.log);
================================================================================
`;

View File

@ -0,0 +1 @@
export default (function log() {} as typeof console.log)