diff --git a/src/comments.js b/src/comments.js index bede556b..7c2179f2 100644 --- a/src/comments.js +++ b/src/comments.js @@ -317,7 +317,7 @@ function printTrailingComment(commentPath, print) { // } // } - parts.push(print(commentPath)); + parts.push(print(commentPath), hardline); return concat(parts); } diff --git a/src/printer.js b/src/printer.js index efbc0246..441febd7 100644 --- a/src/printer.js +++ b/src/printer.js @@ -294,19 +294,9 @@ function genericPrintNoParens(path, options, print) { " = ", path.call(print, "right") ]); - case "MemberExpression": - - parts.push(path.call(print, "object")); - - var property = path.call(print, "property"); - - if (n.computed) { - parts.push("[", property, "]"); - } else { - parts.push(".", property); - } - - return concat(parts); + case "MemberExpression": { + return concat([path.call(print, "object"), printMemberLookup(path, print)]); + } case "MetaProperty": return concat([ path.call(print, "meta"), @@ -620,11 +610,72 @@ function genericPrintNoParens(path, options, print) { parts.push(";"); return concat(parts); - case "CallExpression": + case "CallExpression": { + const parent = path.getParentNode(); + // We detect calls on member expressions specially to format a + // comman pattern better. The pattern we are looking for is this: + // + // arr + // .map(x => x + 1) + // .filter(x => x > 10) + // .some(x => x % 2) + // + // where there is a "chain" of function calls on the result of + // previous expressions. We want to format them like above so they + // are consistently aligned. + // + // The way we do is by eagerly traversing the AST tree and + // re-shape it into a list of calls on member expressions. This + // lets us implement a heuristic easily for when we want to format + // it: if there are more than 1 calls on a member expression that + // pass in a function, we treat it like the above. + if(n.callee.type === "MemberExpression") { + const nodes = []; + let curr = n; + // Traverse down and gather up all of the calls on member + // expressions. This flattens it out into a list that we can + // easily analyze. + while(curr.type === "CallExpression" && curr.callee.type === "MemberExpression") { + nodes.push({ property: curr.callee.property, call: curr }); + curr = curr.callee.object; + } + + // There are two kinds of formats we want to specialize: first, + // if there are multiple calls on lookups we want to group them + // together so they will all break at the same time. Second, + // it's a chain if there 2 or more calls pass in functions and + // we want to forcibly break all of the lookups onto new lines + // and indent them. + const hasMultipleLookups = nodes.length > 1; + const isChain = nodes.filter(n => { + return n.call.arguments.length > 0 && + (n.call.arguments[0].type === "FunctionExpression" || + n.call.arguments[0].type === "ArrowFunctionExpression"); + }).length > 1; + + if(hasMultipleLookups) { + return group(concat([ + print(FastPath.from(curr)), + concat(nodes.reverse().map(node => { + const printed = concat([ + isChain ? hardline : softline, + ".", + print(FastPath.from(node.property)), + printArgumentsList(FastPath.from(node.call), options, print) + ]); + + return isChain ? indent(options.tabWidth, printed) : printed; + })) + ])); + } + } + return concat([ path.call(print, "callee"), printArgumentsList(path, options, print) ]); + } + case "ObjectExpression": case "ObjectPattern": case "ObjectTypeAnnotation": @@ -1914,6 +1965,16 @@ function printClass(path, print) { return parts; } +function printMemberLookup(path, print) { + const property = path.call(print, "property"); + const n = path.getValue(); + return concat( + n.computed ? + ["[", property, "]"] : + [".", property] + ); +} + function adjustClause(clause, options, forceSpace) { if (isCurlyBracket(clause) || forceSpace) { return concat([ " ", clause ]); diff --git a/tests/node_tests/stream/__snapshots__/jsfmt.spec.js.snap b/tests/node_tests/stream/__snapshots__/jsfmt.spec.js.snap index 331dc900..076d5243 100644 --- a/tests/node_tests/stream/__snapshots__/jsfmt.spec.js.snap +++ b/tests/node_tests/stream/__snapshots__/jsfmt.spec.js.snap @@ -67,8 +67,9 @@ class MyWriteStream extends stream.Writable {} class MyDuplex extends stream.Duplex {} class MyTransform extends stream.Duplex {} -new MyReadStream().pipe( - new MyDuplex() -).pipe(new MyTransform()).pipe(new MyWriteStream()); +new MyReadStream() +.pipe(new MyDuplex()) +.pipe(new MyTransform()) +.pipe(new MyWriteStream()); " `; diff --git a/tests/promises/__snapshots__/jsfmt.spec.js.snap b/tests/promises/__snapshots__/jsfmt.spec.js.snap index 724968c3..530fc733 100644 --- a/tests/promises/__snapshots__/jsfmt.spec.js.snap +++ b/tests/promises/__snapshots__/jsfmt.spec.js.snap @@ -529,59 +529,83 @@ Promise.reject(Promise.resolve(0)).then(function(num) { var b: number = num; }); -Promise.resolve(0).then(function(num) { - return \"asdf\"; -}).then(function(str) { - var a: string = str; - var b: number = str; -}); +Promise + .resolve(0) + .then(function(num) { + return \"asdf\"; + }) + .then(function(str) { + var a: string = str; + var b: number = str; + }); -Promise.resolve(0).then(function(num) { - return Promise.resolve(\"asdf\"); -}).then(function(str) { - var a: string = str; - var b: number = str; -}); +Promise + .resolve(0) + .then(function(num) { + return Promise.resolve(\"asdf\"); + }) + .then(function(str) { + var a: string = str; + var b: number = str; + }); -Promise.resolve(0).then(function(num) { - return Promise.resolve(Promise.resolve(\"asdf\")); -}).then(function(str) { - var a: string = str; - var b: number = str; -}); +Promise + .resolve(0) + .then(function(num) { + return Promise.resolve(Promise.resolve(\"asdf\")); + }) + .then(function(str) { + var a: string = str; + var b: number = str; + }); -Promise.resolve(0).then(function(num) { - throw \"str\"; -}).catch(function(str) { - var a: string = str; - var b: number = str; -}); +Promise + .resolve(0) + .then(function(num) { + throw \"str\"; + }) + .catch(function(str) { + var a: string = str; + var b: number = str; + }); -Promise.reject(0).catch(function(num) { - return \"asdf\"; -}).then(function(str) { - var a: string = str; - var b: number = str; -}); +Promise + .reject(0) + .catch(function(num) { + return \"asdf\"; + }) + .then(function(str) { + var a: string = str; + var b: number = str; + }); -Promise.reject(0).catch(function(num) { - return Promise.resolve(\"asdf\"); -}).then(function(str) { - var a: string = str; - var b: number = str; -}); +Promise + .reject(0) + .catch(function(num) { + return Promise.resolve(\"asdf\"); + }) + .then(function(str) { + var a: string = str; + var b: number = str; + }); -Promise.reject(0).catch(function(num) { - return Promise.resolve(Promise.resolve(\"asdf\")); -}).then(function(str) { - var a: string = str; - var b: number = str; -}); +Promise + .reject(0) + .catch(function(num) { + return Promise.resolve(Promise.resolve(\"asdf\")); + }) + .then(function(str) { + var a: string = str; + var b: number = str; + }); -Promise.resolve(0).catch(function(err) {}).then(function(num) { - var a: number = num; - var b: string = num; -}); +Promise + .resolve(0) + .catch(function(err) {}) + .then(function(num) { + var a: number = num; + var b: string = num; + }); " `; diff --git a/tests/union/__snapshots__/jsfmt.spec.js.snap b/tests/union/__snapshots__/jsfmt.spec.js.snap index d3340c89..33a6c05e 100644 --- a/tests/union/__snapshots__/jsfmt.spec.js.snap +++ b/tests/union/__snapshots__/jsfmt.spec.js.snap @@ -86,11 +86,13 @@ exports[`test issue-198.js 1`] = ` // This should fail because str is string, not number var p = new Promise(function(resolve, reject) { resolve(5); -}).then(function(num) { - return num.toFixed(); -}).then(function(str) { - return str.toFixed(); -}); +}) + .then(function(num) { + return num.toFixed(); + }) + .then(function(str) { + return str.toFixed(); + }); " `;