UNPKG

prettierx

Version:

prettierX - a less opinionated fork of the Prettier code formatter

417 lines (381 loc) 12.9 kB
"use strict"; const { printComments } = require("../../main/comments"); const { getLast, isNextLineEmptyAfterIndex, getNextNonSpaceNonCommentCharacterIndex, } = require("../../common/util"); const pathNeedsParens = require("../needs-parens"); const { isCallExpression, isMemberExpression, isFunctionOrArrowExpression, isLongCurriedCallExpression, isMemberish, isNumericLiteral, isSimpleCallArgument, hasComment, CommentCheckFlags, isNextLineEmpty, } = require("../utils"); const { locEnd } = require("../loc"); const { builders: { join, hardline, group, indent, conditionalGroup, breakParent, label, }, utils: { willBreak }, } = require("../../document"); const printCallArguments = require("./call-arguments"); const { printMemberLookup } = require("./member"); const { printOptionalToken, printFunctionTypeParameters, printBindExpressionCallee, } = require("./misc"); // We detect calls on member expressions specially to format a // common pattern better. The pattern we are looking for is this: // // arr // .map(x => x + 1) // .filter(x => x > 10) // .some(x => x % 2) // // The way it is structured in the AST is via a nested sequence of // MemberExpression and CallExpression. We need to traverse the AST // and make groups out of it to print it in the desired way. function printMemberChain(path, options, print) { const parent = path.getParentNode(); const isExpressionStatement = !parent || parent.type === "ExpressionStatement"; // The first phase is to linearize the AST by traversing it down. // // a().b() // has the following AST structure: // CallExpression(MemberExpression(CallExpression(Identifier))) // and we transform it into // [Identifier, CallExpression, MemberExpression, CallExpression] const printedNodes = []; // Here we try to retain one typed empty line after each call expression or // the first group whether it is in parentheses or not function shouldInsertEmptyLineAfter(node) { const { originalText } = options; const nextCharIndex = getNextNonSpaceNonCommentCharacterIndex( originalText, node, locEnd ); const nextChar = originalText.charAt(nextCharIndex); // if it is cut off by a parenthesis, we only account for one typed empty // line after that parenthesis if (nextChar === ")") { return ( nextCharIndex !== false && isNextLineEmptyAfterIndex(originalText, nextCharIndex + 1) ); } return isNextLineEmpty(node, options); } function rec(path) { const node = path.getValue(); if ( isCallExpression(node) && (isMemberish(node.callee) || isCallExpression(node.callee)) ) { printedNodes.unshift({ node, printed: [ printComments( path, [ printOptionalToken(path), printFunctionTypeParameters(path, options, print), printCallArguments(path, options, print), ], options ), shouldInsertEmptyLineAfter(node) ? hardline : "", ], }); path.call((callee) => rec(callee), "callee"); } else if (isMemberish(node)) { printedNodes.unshift({ node, needsParens: pathNeedsParens(path, options), printed: printComments( path, isMemberExpression(node) ? printMemberLookup(path, options, print) : printBindExpressionCallee(path, options, print), options ), }); path.call((object) => rec(object), "object"); } else if (node.type === "TSNonNullExpression") { printedNodes.unshift({ node, printed: printComments(path, "!", options), }); path.call((expression) => rec(expression), "expression"); } else { printedNodes.unshift({ node, printed: print(), }); } } // Note: the comments of the root node have already been printed, so we // need to extract this first call without printing them as they would // if handled inside of the recursive call. const node = path.getValue(); printedNodes.unshift({ node, printed: [ printOptionalToken(path), printFunctionTypeParameters(path, options, print), printCallArguments(path, options, print), ], }); if (node.callee) { path.call((callee) => rec(callee), "callee"); } // Once we have a linear list of printed nodes, we want to create groups out // of it. // // a().b.c().d().e // will be grouped as // [ // [Identifier, CallExpression], // [MemberExpression, MemberExpression, CallExpression], // [MemberExpression, CallExpression], // [MemberExpression], // ] // so that we can print it as // a() // .b.c() // .d() // .e // The first group is the first node followed by // - as many CallExpression as possible // < fn()()() >.something() // - as many array accessors as possible // < fn()[0][1][2] >.something() // - then, as many MemberExpression as possible but the last one // < this.items >.something() const groups = []; let currentGroup = [printedNodes[0]]; let i = 1; for (; i < printedNodes.length; ++i) { if ( printedNodes[i].node.type === "TSNonNullExpression" || isCallExpression(printedNodes[i].node) || (isMemberExpression(printedNodes[i].node) && printedNodes[i].node.computed && isNumericLiteral(printedNodes[i].node.property)) ) { currentGroup.push(printedNodes[i]); } else { break; } } if (!isCallExpression(printedNodes[0].node)) { for (; i + 1 < printedNodes.length; ++i) { if ( isMemberish(printedNodes[i].node) && isMemberish(printedNodes[i + 1].node) ) { currentGroup.push(printedNodes[i]); } else { break; } } } groups.push(currentGroup); currentGroup = []; // Then, each following group is a sequence of MemberExpression followed by // a sequence of CallExpression. To compute it, we keep adding things to the // group until we has seen a CallExpression in the past and reach a // MemberExpression let hasSeenCallExpression = false; for (; i < printedNodes.length; ++i) { if (hasSeenCallExpression && isMemberish(printedNodes[i].node)) { // [0] should be appended at the end of the group instead of the // beginning of the next one if ( printedNodes[i].node.computed && isNumericLiteral(printedNodes[i].node.property) ) { currentGroup.push(printedNodes[i]); continue; } groups.push(currentGroup); currentGroup = []; hasSeenCallExpression = false; } if ( isCallExpression(printedNodes[i].node) || printedNodes[i].node.type === "ImportExpression" ) { hasSeenCallExpression = true; } currentGroup.push(printedNodes[i]); if (hasComment(printedNodes[i].node, CommentCheckFlags.Trailing)) { groups.push(currentGroup); currentGroup = []; hasSeenCallExpression = false; } } if (currentGroup.length > 0) { groups.push(currentGroup); } // There are cases like Object.keys(), Observable.of(), _.values() where // they are the subject of all the chained calls and therefore should // be kept on the same line: // // Object.keys(items) // .filter(x => x) // .map(x => x) // // In order to detect those cases, we use an heuristic: if the first // node is an identifier with the name starting with a capital // letter or just a sequence of _$. The rationale is that they are // likely to be factories. function isFactory(name) { return /^[A-Z]|^[$_]+$/.test(name); } // In case the Identifier is shorter than tab width, we can keep the // first call in a single line, if it's an ExpressionStatement. // // d3.scaleLinear() // .domain([0, 100]) // .range([0, width]); // function isShort(name) { return name.length <= options.tabWidth; } function shouldNotWrap(groups) { const hasComputed = groups[1].length > 0 && groups[1][0].node.computed; if (groups[0].length === 1) { const firstNode = groups[0][0].node; return ( firstNode.type === "ThisExpression" || (firstNode.type === "Identifier" && (isFactory(firstNode.name) || (isExpressionStatement && isShort(firstNode.name)) || hasComputed)) ); } const lastNode = getLast(groups[0]).node; return ( isMemberExpression(lastNode) && lastNode.property.type === "Identifier" && (isFactory(lastNode.property.name) || hasComputed) ); } const shouldMerge = groups.length >= 2 && !hasComment(groups[1][0].node) && shouldNotWrap(groups); function printGroup(printedGroup) { const printed = printedGroup.map((tuple) => tuple.printed); // Checks if the last node (i.e. the parent node) needs parens and print // accordingly if (printedGroup.length > 0 && getLast(printedGroup).needsParens) { return ["(", ...printed, ")"]; } return printed; } function printIndentedGroup(groups) { /* istanbul ignore next */ if (groups.length === 0) { return ""; } // [prettierx]: --no-indent-chains option support const printed = group([hardline, join(hardline, groups.map(printGroup))]); return options.indentChains ? indent(printed) : printed; } const printedGroups = groups.map(printGroup); const oneLine = printedGroups; const cutoff = shouldMerge ? 3 : 2; const flatGroups = groups.flat(); const nodeHasComment = flatGroups .slice(1, -1) .some((node) => hasComment(node.node, CommentCheckFlags.Leading)) || flatGroups .slice(0, -1) .some((node) => hasComment(node.node, CommentCheckFlags.Trailing)) || (groups[cutoff] && hasComment(groups[cutoff][0].node, CommentCheckFlags.Leading)); // If we only have a single `.`, we shouldn't do anything fancy and just // render everything concatenated together. if (groups.length <= cutoff && !nodeHasComment) { if (isLongCurriedCallExpression(path)) { return oneLine; } return group(oneLine); } // Find out the last node in the first group and check if it has an // empty line after const lastNodeBeforeIndent = getLast(groups[shouldMerge ? 1 : 0]).node; const shouldHaveEmptyLineBeforeIndent = !isCallExpression(lastNodeBeforeIndent) && shouldInsertEmptyLineAfter(lastNodeBeforeIndent); const expanded = [ printGroup(groups[0]), shouldMerge ? groups.slice(1, 2).map(printGroup) : "", shouldHaveEmptyLineBeforeIndent ? hardline : "", printIndentedGroup(groups.slice(shouldMerge ? 2 : 1)), ]; const callExpressions = printedNodes .map(({ node }) => node) .filter(isCallExpression); function lastGroupWillBreakAndOtherCallsHaveFunctionArguments() { const lastGroupNode = getLast(getLast(groups)).node; const lastGroupDoc = getLast(printedGroups); return ( isCallExpression(lastGroupNode) && willBreak(lastGroupDoc) && callExpressions .slice(0, -1) .some((node) => node.arguments.some(isFunctionOrArrowExpression)) ); } let result; // We don't want to print in one line if at least one of these conditions occurs: // * the chain has comments, // * [prettierx] --break-long-method-chains option if enabled: // the chain has at least 3 chained method calls, // * the chain is an expression statement and all the arguments are literal-like ("fluent configuration" pattern), // * the chain is longer than 2 calls and has non-trivial arguments or more than 2 arguments in any call but the first one, // * any group but the last one has a hard line, // * the last call's arguments have a hard line and other calls have non-trivial arguments. if ( nodeHasComment || // [prettierx] breakLongMethodChains option support (options.breakLongMethodChains && callExpressions.length >= 3) || (callExpressions.length > 2 && callExpressions.some( (expr) => !expr.arguments.every((arg) => isSimpleCallArgument(arg, 0)) )) || printedGroups.slice(0, -1).some(willBreak) || lastGroupWillBreakAndOtherCallsHaveFunctionArguments() ) { result = group(expanded); } else { result = [ // We only need to check `oneLine` because if `expanded` is chosen // that means that the parent group has already been broken // naturally willBreak(oneLine) || shouldHaveEmptyLineBeforeIndent ? breakParent : "", conditionalGroup([oneLine, expanded]), ]; } return label("member-chain", result); } module.exports = printMemberChain;