UNPKG

prettierx

Version:

prettierX - a less opinionated fork of the Prettier code formatter

611 lines (532 loc) 17.5 kB
"use strict"; const assert = require("assert"); const { builders: { line, hardline, breakParent, indent, lineSuffix, join, cursor }, } = require("../document"); const { hasNewline, skipNewline, skipSpaces, isPreviousLineEmpty, addLeadingComment, addDanglingComment, addTrailingComment, } = require("../common/util"); const childNodesCache = new WeakMap(); function getSortedChildNodes(node, options, resultArray) { if (!node) { return; } const { printer, locStart, locEnd } = options; if (resultArray) { if (printer.canAttachComment && printer.canAttachComment(node)) { // This reverse insertion sort almost always takes constant // time because we almost always (maybe always?) append the // nodes in order anyway. let i; for (i = resultArray.length - 1; i >= 0; --i) { if ( locStart(resultArray[i]) <= locStart(node) && locEnd(resultArray[i]) <= locEnd(node) ) { break; } } resultArray.splice(i + 1, 0, node); return; } } else if (childNodesCache.has(node)) { return childNodesCache.get(node); } const childNodes = (printer.getCommentChildNodes && printer.getCommentChildNodes(node, options)) || (typeof node === "object" && Object.entries(node) .filter( ([key]) => key !== "enclosingNode" && key !== "precedingNode" && key !== "followingNode" && key !== "tokens" && key !== "comments" ) .map(([, value]) => value)); if (!childNodes) { return; } if (!resultArray) { resultArray = []; childNodesCache.set(node, resultArray); } for (const childNode of childNodes) { getSortedChildNodes(childNode, options, resultArray); } return resultArray; } // As efficiently as possible, decorate the comment object with // .precedingNode, .enclosingNode, and/or .followingNode properties, at // least one of which is guaranteed to be defined. function decorateComment(node, comment, options, enclosingNode) { const { locStart, locEnd } = options; const commentStart = locStart(comment); const commentEnd = locEnd(comment); const childNodes = getSortedChildNodes(node, options); let precedingNode; let followingNode; // Time to dust off the old binary search robes and wizard hat. let left = 0; let right = childNodes.length; while (left < right) { const middle = (left + right) >> 1; const child = childNodes[middle]; const start = locStart(child); const end = locEnd(child); // The comment is completely contained by this child node. if (start <= commentStart && commentEnd <= end) { // Abandon the binary search at this level. return decorateComment(child, comment, options, child); } if (end <= commentStart) { // This child node falls completely before the comment. // Because we will never consider this node or any nodes // before it again, this node must be the closest preceding // node we have encountered so far. precedingNode = child; left = middle + 1; continue; } if (commentEnd <= start) { // This child node falls completely after the comment. // Because we will never consider this node or any nodes after // it again, this node must be the closest following node we // have encountered so far. followingNode = child; right = middle; continue; } /* istanbul ignore next */ throw new Error("Comment location overlaps with node location"); } // We don't want comments inside of different expressions inside of the same // template literal to move to another expression. if (enclosingNode && enclosingNode.type === "TemplateLiteral") { const { quasis } = enclosingNode; const commentIndex = findExpressionIndexForComment( quasis, comment, options ); if ( precedingNode && findExpressionIndexForComment(quasis, precedingNode, options) !== commentIndex ) { precedingNode = null; } if ( followingNode && findExpressionIndexForComment(quasis, followingNode, options) !== commentIndex ) { followingNode = null; } } return { enclosingNode, precedingNode, followingNode }; } const returnFalse = () => false; function attach(comments, ast, text, options) { if (!Array.isArray(comments)) { return; } const tiesToBreak = []; const { locStart, locEnd, printer: { handleComments = {} }, } = options; // TODO: Make this as default behavior const { avoidAstMutation, ownLine: handleOwnLineComment = returnFalse, endOfLine: handleEndOfLineComment = returnFalse, remaining: handleRemainingComment = returnFalse, } = handleComments; const decoratedComments = comments.map((comment, index) => ({ ...decorateComment(ast, comment, options), comment, text, options, ast, isLastComment: comments.length - 1 === index, })); for (const [index, context] of decoratedComments.entries()) { const { comment, precedingNode, enclosingNode, followingNode, text, options, ast, isLastComment, } = context; if ( options.parser === "json" || options.parser === "json5" || options.parser === "__js_expression" || options.parser === "__vue_expression" ) { if (locStart(comment) - locStart(ast) <= 0) { addLeadingComment(ast, comment); continue; } if (locEnd(comment) - locEnd(ast) >= 0) { addTrailingComment(ast, comment); continue; } } let args; if (avoidAstMutation) { args = [context]; } else { comment.enclosingNode = enclosingNode; comment.precedingNode = precedingNode; comment.followingNode = followingNode; args = [comment, text, options, ast, isLastComment]; } if (isOwnLineComment(text, options, decoratedComments, index)) { comment.placement = "ownLine"; // If a comment exists on its own line, prefer a leading comment. // We also need to check if it's the first line of the file. if (handleOwnLineComment(...args)) { // We're good } else if (followingNode) { // Always a leading comment. addLeadingComment(followingNode, comment); } else if (precedingNode) { addTrailingComment(precedingNode, comment); } else if (enclosingNode) { addDanglingComment(enclosingNode, comment); } else { // There are no nodes, let's attach it to the root of the ast /* istanbul ignore next */ addDanglingComment(ast, comment); } } else if (isEndOfLineComment(text, options, decoratedComments, index)) { comment.placement = "endOfLine"; if (handleEndOfLineComment(...args)) { // We're good } else if (precedingNode) { // There is content before this comment on the same line, but // none after it, so prefer a trailing comment of the previous node. addTrailingComment(precedingNode, comment); } else if (followingNode) { addLeadingComment(followingNode, comment); } else if (enclosingNode) { addDanglingComment(enclosingNode, comment); } else { // There are no nodes, let's attach it to the root of the ast /* istanbul ignore next */ addDanglingComment(ast, comment); } } else { comment.placement = "remaining"; if (handleRemainingComment(...args)) { // We're good } else if (precedingNode && followingNode) { // Otherwise, text exists both before and after the comment on // the same line. If there is both a preceding and following // node, use a tie-breaking algorithm to determine if it should // be attached to the next or previous node. In the last case, // simply attach the right node; const tieCount = tiesToBreak.length; if (tieCount > 0) { const lastTie = tiesToBreak[tieCount - 1]; if (lastTie.followingNode !== followingNode) { breakTies(tiesToBreak, text, options); } } tiesToBreak.push(context); } else if (precedingNode) { addTrailingComment(precedingNode, comment); } else if (followingNode) { addLeadingComment(followingNode, comment); } else if (enclosingNode) { addDanglingComment(enclosingNode, comment); } else { // There are no nodes, let's attach it to the root of the ast /* istanbul ignore next */ addDanglingComment(ast, comment); } } } breakTies(tiesToBreak, text, options); if (!avoidAstMutation) { for (const comment of comments) { // These node references were useful for breaking ties, but we // don't need them anymore, and they create cycles in the AST that // may lead to infinite recursion if we don't delete them here. delete comment.precedingNode; delete comment.enclosingNode; delete comment.followingNode; } } } const isAllEmptyAndNoLineBreak = (text) => !/[\S\n\u2028\u2029]/.test(text); function isOwnLineComment(text, options, decoratedComments, commentIndex) { const { comment, precedingNode } = decoratedComments[commentIndex]; const { locStart, locEnd } = options; let start = locStart(comment); if (precedingNode) { // Find first comment on the same line for (let index = commentIndex - 1; index >= 0; index--) { const { comment, precedingNode: currentCommentPrecedingNode } = decoratedComments[index]; if ( currentCommentPrecedingNode !== precedingNode || !isAllEmptyAndNoLineBreak(text.slice(locEnd(comment), start)) ) { break; } start = locStart(comment); } } return hasNewline(text, start, { backwards: true }); } function isEndOfLineComment(text, options, decoratedComments, commentIndex) { const { comment, followingNode } = decoratedComments[commentIndex]; const { locStart, locEnd } = options; let end = locEnd(comment); if (followingNode) { // Find last comment on the same line for ( let index = commentIndex + 1; index < decoratedComments.length; index++ ) { const { comment, followingNode: currentCommentFollowingNode } = decoratedComments[index]; if ( currentCommentFollowingNode !== followingNode || !isAllEmptyAndNoLineBreak(text.slice(end, locStart(comment))) ) { break; } end = locEnd(comment); } } return hasNewline(text, end); } function breakTies(tiesToBreak, text, options) { const tieCount = tiesToBreak.length; if (tieCount === 0) { return; } const { precedingNode, followingNode, enclosingNode } = tiesToBreak[0]; const gapRegExp = (options.printer.getGapRegex && options.printer.getGapRegex(enclosingNode)) || /^[\s(]*$/; let gapEndPos = options.locStart(followingNode); // 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 // gaps (or other comments). Gaps should only contain whitespace or open // parentheses. let indexOfFirstLeadingComment; for ( indexOfFirstLeadingComment = tieCount; indexOfFirstLeadingComment > 0; --indexOfFirstLeadingComment ) { const { comment, precedingNode: currentCommentPrecedingNode, followingNode: currentCommentFollowingNode, } = tiesToBreak[indexOfFirstLeadingComment - 1]; assert.strictEqual(currentCommentPrecedingNode, precedingNode); assert.strictEqual(currentCommentFollowingNode, followingNode); const gap = text.slice(options.locEnd(comment), gapEndPos); if (gapRegExp.test(gap)) { gapEndPos = options.locStart(comment); } else { // The gap string contained something other than whitespace or open // parentheses. break; } } for (const [i, { comment }] of tiesToBreak.entries()) { if (i < indexOfFirstLeadingComment) { addTrailingComment(precedingNode, comment); } else { addLeadingComment(followingNode, comment); } } for (const node of [precedingNode, followingNode]) { if (node.comments && node.comments.length > 1) { node.comments.sort((a, b) => options.locStart(a) - options.locStart(b)); } } tiesToBreak.length = 0; } function printComment(path, options) { const comment = path.getValue(); comment.printed = true; return options.printer.printComment(path, options); } function findExpressionIndexForComment(quasis, comment, options) { const startPos = options.locStart(comment) - 1; for (let i = 1; i < quasis.length; ++i) { if (startPos < options.locStart(quasis[i])) { return i - 1; } } // We haven't found it, it probably means that some of the locations are off. // Let's just return the first one. /* istanbul ignore next */ return 0; } function printLeadingComment(path, options) { const comment = path.getValue(); const parts = [printComment(path, options)]; const { printer, originalText, locStart, locEnd } = options; const isBlock = printer.isBlockComment && printer.isBlockComment(comment); // Leading block comments should see if they need to stay on the // same line or not. if (isBlock) { const lineBreak = hasNewline(originalText, locEnd(comment)) ? hasNewline(originalText, locStart(comment), { backwards: true, }) ? hardline : line : " "; parts.push(lineBreak); } else { parts.push(hardline); } const index = skipNewline( originalText, skipSpaces(originalText, locEnd(comment)) ); if (index !== false && hasNewline(originalText, index)) { parts.push(hardline); } return parts; } function printTrailingComment(path, options) { const comment = path.getValue(); const printed = printComment(path, options); const { printer, originalText, locStart } = options; const isBlock = printer.isBlockComment && printer.isBlockComment(comment); if (hasNewline(originalText, locStart(comment), { backwards: true })) { // This allows comments at the end of nested structures: // { // x: 1, // y: 2 // // A comment // } // Those kinds of comments are almost always leading comments, but // here it doesn't go "outside" the block and turns it into a // trailing comment for `2`. We can simulate the above by checking // if this a comment on its own line; normal trailing comments are // always at the end of another expression. const isLineBeforeEmpty = isPreviousLineEmpty( originalText, comment, locStart ); return lineSuffix([hardline, isLineBeforeEmpty ? hardline : "", printed]); } let parts = [" ", printed]; // Trailing block comments never need a newline if (!isBlock) { parts = [lineSuffix(parts), breakParent]; } return parts; } function printDanglingComments(path, options, sameIndent, filter) { const parts = []; const node = path.getValue(); if (!node || !node.comments) { return ""; } path.each(() => { const comment = path.getValue(); if (!comment.leading && !comment.trailing && (!filter || filter(comment))) { parts.push(printComment(path, options)); } }, "comments"); if (parts.length === 0) { return ""; } if (sameIndent) { return join(hardline, parts); } return indent([hardline, join(hardline, parts)]); } function printCommentsSeparately(path, options, ignored) { const value = path.getValue(); if (!value) { return {}; } let comments = value.comments || []; if (ignored) { comments = comments.filter((comment) => !ignored.has(comment)); } const isCursorNode = value === options.cursorNode; if (comments.length === 0) { const maybeCursor = isCursorNode ? cursor : ""; return { leading: maybeCursor, trailing: maybeCursor }; } const leadingParts = []; const trailingParts = []; path.each(() => { const comment = path.getValue(); if (ignored && ignored.has(comment)) { return; } const { leading, trailing } = comment; if (leading) { leadingParts.push(printLeadingComment(path, options)); } else if (trailing) { trailingParts.push(printTrailingComment(path, options)); } }, "comments"); if (isCursorNode) { leadingParts.unshift(cursor); trailingParts.push(cursor); } return { leading: leadingParts, trailing: trailingParts }; } function printComments(path, doc, options, ignored) { const { leading, trailing } = printCommentsSeparately(path, options, ignored); if (!leading && !trailing) { return doc; } return [leading, doc, trailing]; } function ensureAllCommentsPrinted(astComments) { if (!astComments) { return; } for (const comment of astComments) { if (!comment.printed) { throw new Error( 'Comment "' + comment.value.trim() + '" was not printed. Please report this error!' ); } delete comment.printed; } } module.exports = { attach, printComments, printCommentsSeparately, printDanglingComments, getSortedChildNodes, ensureAllCommentsPrinted, };