prettierx
Version:
prettierX - a less opinionated fork of the Prettier code formatter
986 lines (909 loc) • 25.3 kB
JavaScript
"use strict";
const {
getLast,
hasNewline,
getNextNonSpaceNonCommentCharacterIndexWithStartIndex,
getNextNonSpaceNonCommentCharacter,
hasNewlineInRange,
addLeadingComment,
addTrailingComment,
addDanglingComment,
getNextNonSpaceNonCommentCharacterIndex,
isNonEmptyArray,
} = require("../common/util");
const {
isBlockComment,
getFunctionParameters,
isPrettierIgnoreComment,
isJsxNode,
hasFlowShorthandAnnotationComment,
hasFlowAnnotationComment,
hasIgnoreComment,
isCallLikeExpression,
getCallArguments,
isCallExpression,
isMemberExpression,
isObjectProperty,
} = require("./utils");
const { locStart, locEnd } = require("./loc");
/**
* @typedef {import("./types/estree").Node} Node
* @typedef {import("./types/estree").Comment} Comment
* @typedef {import("../common/ast-path")} AstPath
*
* @typedef {Object} CommentContext
* @property {Comment} comment
* @property {Node} precedingNode
* @property {Node} enclosingNode
* @property {Node} followingNode
* @property {string} text
* @property {any} options
* @property {Node} ast
* @property {boolean} isLastComment
*/
/**
* @param {CommentContext} context
* @returns {boolean}
*/
function handleOwnLineComment(context) {
return [
handleIgnoreComments,
handleLastFunctionArgComments,
handleMemberExpressionComments,
handleIfStatementComments,
handleWhileComments,
handleTryStatementComments,
handleClassComments,
handleImportSpecifierComments,
handleForComments,
handleUnionTypeComments,
handleOnlyComments,
handleImportDeclarationComments,
handleAssignmentPatternComments,
handleMethodNameComments,
handleLabeledStatementComments,
].some((fn) => fn(context));
}
/**
* @param {CommentContext} context
* @returns {boolean}
*/
function handleEndOfLineComment(context) {
return [
handleClosureTypeCastComments,
handleLastFunctionArgComments,
handleConditionalExpressionComments,
handleImportSpecifierComments,
handleIfStatementComments,
handleWhileComments,
handleTryStatementComments,
handleClassComments,
handleLabeledStatementComments,
handleCallExpressionComments,
handlePropertyComments,
handleOnlyComments,
handleTypeAliasComments,
handleVariableDeclaratorComments,
].some((fn) => fn(context));
}
/**
* @param {CommentContext} context
* @returns {boolean}
*/
function handleRemainingComment(context) {
return [
handleIgnoreComments,
handleIfStatementComments,
handleWhileComments,
handleObjectPropertyAssignment,
handleCommentInEmptyParens,
handleMethodNameComments,
handleOnlyComments,
handleCommentAfterArrowParams,
handleFunctionNameComments,
handleTSMappedTypeComments,
handleBreakAndContinueStatementComments,
handleTSFunctionTrailingComments,
].some((fn) => fn(context));
}
/**
* @param {Node} node
* @returns {void}
*/
function addBlockStatementFirstComment(node, comment) {
// @ts-ignore
const firstNonEmptyNode = (node.body || node.properties).find(
({ type }) => type !== "EmptyStatement"
);
if (firstNonEmptyNode) {
addLeadingComment(firstNonEmptyNode, comment);
} else {
addDanglingComment(node, comment);
}
}
/**
* @param {Node} node
* @returns {void}
*/
function addBlockOrNotComment(node, comment) {
if (node.type === "BlockStatement") {
addBlockStatementFirstComment(node, comment);
} else {
addLeadingComment(node, comment);
}
}
function handleClosureTypeCastComments({ comment, followingNode }) {
if (followingNode && isTypeCastComment(comment)) {
addLeadingComment(followingNode, comment);
return true;
}
return false;
}
// There are often comments before the else clause of if statements like
//
// if (1) { ... }
// // comment
// else { ... }
//
// They are being attached as leading comments of the BlockExpression which
// is not well printed. What we want is to instead move the comment inside
// of the block and make it leadingComment of the first element of the block
// or dangling comment of the block if there is nothing inside
//
// if (1) { ... }
// else {
// // comment
// ...
// }
function handleIfStatementComments({
comment,
precedingNode,
enclosingNode,
followingNode,
text,
// [prettierx] support --break-before-else option
options,
}) {
if (
!enclosingNode ||
enclosingNode.type !== "IfStatement" ||
!followingNode
) {
return false;
}
// We unfortunately have no way using the AST or location of nodes to know
// if the comment is positioned before the condition parenthesis:
// if (a /* comment */) {}
// The only workaround I found is to look at the next character to see if
// it is a ).
const nextCharacter = getNextNonSpaceNonCommentCharacter(
text,
comment,
locEnd
);
if (nextCharacter === ")") {
addTrailingComment(precedingNode, comment);
return true;
}
// Comments before `else`:
// - treat as trailing comments of the consequent, if it's a BlockStatement
// - treat as a dangling comment otherwise
if (
precedingNode === enclosingNode.consequent &&
followingNode === enclosingNode.alternate
) {
// [prettierx] --break-before-else option support
if (precedingNode.type === "BlockStatement" && !options.breakBeforeElse) {
addTrailingComment(precedingNode, comment);
} else {
addDanglingComment(enclosingNode, comment);
}
return true;
}
if (followingNode.type === "BlockStatement") {
addBlockStatementFirstComment(followingNode, comment);
return true;
}
if (followingNode.type === "IfStatement") {
addBlockOrNotComment(followingNode.consequent, comment);
return true;
}
// For comments positioned after the condition parenthesis in an if statement
// before the consequent without brackets on, such as
// if (a) /* comment */ true,
// we look at the next character to see if the following node
// is the consequent for the if statement
if (enclosingNode.consequent === followingNode) {
addLeadingComment(followingNode, comment);
return true;
}
return false;
}
function handleWhileComments({
comment,
precedingNode,
enclosingNode,
followingNode,
text,
}) {
if (
!enclosingNode ||
enclosingNode.type !== "WhileStatement" ||
!followingNode
) {
return false;
}
// We unfortunately have no way using the AST or location of nodes to know
// if the comment is positioned before the condition parenthesis:
// while (a /* comment */) {}
// The only workaround I found is to look at the next character to see if
// it is a ).
const nextCharacter = getNextNonSpaceNonCommentCharacter(
text,
comment,
locEnd
);
if (nextCharacter === ")") {
addTrailingComment(precedingNode, comment);
return true;
}
if (followingNode.type === "BlockStatement") {
addBlockStatementFirstComment(followingNode, comment);
return true;
}
if (enclosingNode.body === followingNode) {
addLeadingComment(followingNode, comment);
return true;
}
return false;
}
// Same as IfStatement but for TryStatement
function handleTryStatementComments({
comment,
precedingNode,
enclosingNode,
followingNode,
}) {
if (
!enclosingNode ||
(enclosingNode.type !== "TryStatement" &&
enclosingNode.type !== "CatchClause") ||
!followingNode
) {
return false;
}
if (enclosingNode.type === "CatchClause" && precedingNode) {
addTrailingComment(precedingNode, comment);
return true;
}
if (followingNode.type === "BlockStatement") {
addBlockStatementFirstComment(followingNode, comment);
return true;
}
if (followingNode.type === "TryStatement") {
addBlockOrNotComment(followingNode.finalizer, comment);
return true;
}
if (followingNode.type === "CatchClause") {
addBlockOrNotComment(followingNode.body, comment);
return true;
}
return false;
}
function handleMemberExpressionComments({
comment,
enclosingNode,
followingNode,
}) {
if (
isMemberExpression(enclosingNode) &&
followingNode &&
followingNode.type === "Identifier"
) {
addLeadingComment(enclosingNode, comment);
return true;
}
return false;
}
function handleConditionalExpressionComments({
comment,
precedingNode,
enclosingNode,
followingNode,
text,
}) {
const isSameLineAsPrecedingNode =
precedingNode &&
!hasNewlineInRange(text, locEnd(precedingNode), locStart(comment));
if (
(!precedingNode || !isSameLineAsPrecedingNode) &&
enclosingNode &&
(enclosingNode.type === "ConditionalExpression" ||
enclosingNode.type === "TSConditionalType") &&
followingNode
) {
addLeadingComment(followingNode, comment);
return true;
}
return false;
}
function handleObjectPropertyAssignment({
comment,
precedingNode,
enclosingNode,
}) {
if (
isObjectProperty(enclosingNode) &&
enclosingNode.shorthand &&
enclosingNode.key === precedingNode &&
enclosingNode.value.type === "AssignmentPattern"
) {
addTrailingComment(enclosingNode.value.left, comment);
return true;
}
return false;
}
function handleClassComments({
comment,
precedingNode,
enclosingNode,
followingNode,
}) {
if (
enclosingNode &&
(enclosingNode.type === "ClassDeclaration" ||
enclosingNode.type === "ClassExpression" ||
enclosingNode.type === "DeclareClass" ||
enclosingNode.type === "DeclareInterface" ||
enclosingNode.type === "InterfaceDeclaration" ||
enclosingNode.type === "TSInterfaceDeclaration")
) {
if (
isNonEmptyArray(enclosingNode.decorators) &&
!(followingNode && followingNode.type === "Decorator")
) {
addTrailingComment(getLast(enclosingNode.decorators), comment);
return true;
}
if (enclosingNode.body && followingNode === enclosingNode.body) {
addBlockStatementFirstComment(enclosingNode.body, comment);
return true;
}
// Don't add leading comments to `implements`, `extends`, `mixins` to
// avoid printing the comment after the keyword.
if (followingNode) {
for (const prop of ["implements", "extends", "mixins"]) {
if (enclosingNode[prop] && followingNode === enclosingNode[prop][0]) {
if (
precedingNode &&
(precedingNode === enclosingNode.id ||
precedingNode === enclosingNode.typeParameters ||
precedingNode === enclosingNode.superClass)
) {
addTrailingComment(precedingNode, comment);
} else {
addDanglingComment(enclosingNode, comment, prop);
}
return true;
}
}
}
}
return false;
}
function handleMethodNameComments({
comment,
precedingNode,
enclosingNode,
text,
}) {
// This is only needed for estree parsers (flow, typescript) to attach
// after a method name:
// obj = { fn /*comment*/() {} };
if (
enclosingNode &&
precedingNode &&
// "MethodDefinition" is handled in getCommentChildNodes
(enclosingNode.type === "Property" ||
enclosingNode.type === "TSDeclareMethod" ||
enclosingNode.type === "TSAbstractMethodDefinition") &&
precedingNode.type === "Identifier" &&
enclosingNode.key === precedingNode &&
// special Property case: { key: /*comment*/(value) };
// comment should be attached to value instead of key
getNextNonSpaceNonCommentCharacter(text, precedingNode, locEnd) !== ":"
) {
addTrailingComment(precedingNode, comment);
return true;
}
// Print comments between decorators and class methods as a trailing comment
// on the decorator node instead of the method node
if (
precedingNode &&
enclosingNode &&
precedingNode.type === "Decorator" &&
(enclosingNode.type === "ClassMethod" ||
enclosingNode.type === "ClassProperty" ||
enclosingNode.type === "PropertyDefinition" ||
enclosingNode.type === "TSAbstractClassProperty" ||
enclosingNode.type === "TSAbstractMethodDefinition" ||
enclosingNode.type === "TSDeclareMethod" ||
enclosingNode.type === "MethodDefinition")
) {
addTrailingComment(precedingNode, comment);
return true;
}
return false;
}
function handleFunctionNameComments({
comment,
precedingNode,
enclosingNode,
text,
}) {
if (getNextNonSpaceNonCommentCharacter(text, comment, locEnd) !== "(") {
return false;
}
if (
precedingNode &&
enclosingNode &&
(enclosingNode.type === "FunctionDeclaration" ||
enclosingNode.type === "FunctionExpression" ||
enclosingNode.type === "ClassMethod" ||
enclosingNode.type === "MethodDefinition" ||
enclosingNode.type === "ObjectMethod")
) {
addTrailingComment(precedingNode, comment);
return true;
}
return false;
}
function handleCommentAfterArrowParams({ comment, enclosingNode, text }) {
if (!(enclosingNode && enclosingNode.type === "ArrowFunctionExpression")) {
return false;
}
const index = getNextNonSpaceNonCommentCharacterIndex(text, comment, locEnd);
if (index !== false && text.slice(index, index + 2) === "=>") {
addDanglingComment(enclosingNode, comment);
return true;
}
return false;
}
function handleCommentInEmptyParens({ comment, enclosingNode, text }) {
if (getNextNonSpaceNonCommentCharacter(text, comment, locEnd) !== ")") {
return false;
}
// Only add dangling comments to fix the case when no params are present,
// i.e. a function without any argument.
if (
enclosingNode &&
((isRealFunctionLikeNode(enclosingNode) &&
getFunctionParameters(enclosingNode).length === 0) ||
(isCallLikeExpression(enclosingNode) &&
getCallArguments(enclosingNode).length === 0))
) {
addDanglingComment(enclosingNode, comment);
return true;
}
if (
enclosingNode &&
(enclosingNode.type === "MethodDefinition" ||
enclosingNode.type === "TSAbstractMethodDefinition") &&
getFunctionParameters(enclosingNode.value).length === 0
) {
addDanglingComment(enclosingNode.value, comment);
return true;
}
return false;
}
function handleLastFunctionArgComments({
comment,
precedingNode,
enclosingNode,
followingNode,
text,
}) {
// Flow function type definitions
if (
precedingNode &&
precedingNode.type === "FunctionTypeParam" &&
enclosingNode &&
enclosingNode.type === "FunctionTypeAnnotation" &&
followingNode &&
followingNode.type !== "FunctionTypeParam"
) {
addTrailingComment(precedingNode, comment);
return true;
}
// Real functions and TypeScript function type definitions
if (
precedingNode &&
(precedingNode.type === "Identifier" ||
precedingNode.type === "AssignmentPattern") &&
enclosingNode &&
isRealFunctionLikeNode(enclosingNode) &&
getNextNonSpaceNonCommentCharacter(text, comment, locEnd) === ")"
) {
addTrailingComment(precedingNode, comment);
return true;
}
if (
enclosingNode &&
enclosingNode.type === "FunctionDeclaration" &&
followingNode &&
followingNode.type === "BlockStatement"
) {
const functionParamRightParenIndex = (() => {
const parameters = getFunctionParameters(enclosingNode);
if (parameters.length > 0) {
return getNextNonSpaceNonCommentCharacterIndexWithStartIndex(
text,
locEnd(getLast(parameters))
);
}
const functionParamLeftParenIndex =
getNextNonSpaceNonCommentCharacterIndexWithStartIndex(
text,
locEnd(enclosingNode.id)
);
return (
functionParamLeftParenIndex !== false &&
getNextNonSpaceNonCommentCharacterIndexWithStartIndex(
text,
functionParamLeftParenIndex + 1
)
);
})();
if (locStart(comment) > functionParamRightParenIndex) {
addBlockStatementFirstComment(followingNode, comment);
return true;
}
}
return false;
}
function handleImportSpecifierComments({ comment, enclosingNode }) {
if (enclosingNode && enclosingNode.type === "ImportSpecifier") {
addLeadingComment(enclosingNode, comment);
return true;
}
return false;
}
function handleLabeledStatementComments({ comment, enclosingNode }) {
if (enclosingNode && enclosingNode.type === "LabeledStatement") {
addLeadingComment(enclosingNode, comment);
return true;
}
return false;
}
function handleBreakAndContinueStatementComments({ comment, enclosingNode }) {
if (
enclosingNode &&
(enclosingNode.type === "ContinueStatement" ||
enclosingNode.type === "BreakStatement") &&
!enclosingNode.label
) {
addTrailingComment(enclosingNode, comment);
return true;
}
return false;
}
function handleCallExpressionComments({
comment,
precedingNode,
enclosingNode,
}) {
if (
isCallExpression(enclosingNode) &&
precedingNode &&
enclosingNode.callee === precedingNode &&
enclosingNode.arguments.length > 0
) {
addLeadingComment(enclosingNode.arguments[0], comment);
return true;
}
return false;
}
function handleUnionTypeComments({
comment,
precedingNode,
enclosingNode,
followingNode,
}) {
if (
enclosingNode &&
(enclosingNode.type === "UnionTypeAnnotation" ||
enclosingNode.type === "TSUnionType")
) {
if (isPrettierIgnoreComment(comment)) {
followingNode.prettierIgnore = true;
comment.unignore = true;
}
if (precedingNode) {
addTrailingComment(precedingNode, comment);
return true;
}
return false;
}
if (
followingNode &&
(followingNode.type === "UnionTypeAnnotation" ||
followingNode.type === "TSUnionType") &&
isPrettierIgnoreComment(comment)
) {
followingNode.types[0].prettierIgnore = true;
comment.unignore = true;
}
return false;
}
function handlePropertyComments({ comment, enclosingNode }) {
if (isObjectProperty(enclosingNode)) {
addLeadingComment(enclosingNode, comment);
return true;
}
return false;
}
function handleOnlyComments({
comment,
enclosingNode,
followingNode,
ast,
isLastComment,
}) {
// With Flow the enclosingNode is undefined so use the AST instead.
if (ast && ast.body && ast.body.length === 0) {
if (isLastComment) {
addDanglingComment(ast, comment);
} else {
addLeadingComment(ast, comment);
}
return true;
}
if (
enclosingNode &&
enclosingNode.type === "Program" &&
enclosingNode.body.length === 0 &&
!isNonEmptyArray(enclosingNode.directives)
) {
if (isLastComment) {
addDanglingComment(enclosingNode, comment);
} else {
addLeadingComment(enclosingNode, comment);
}
return true;
}
if (
followingNode &&
followingNode.type === "Program" &&
followingNode.body.length === 0 &&
enclosingNode &&
enclosingNode.type === "ModuleExpression"
) {
addDanglingComment(followingNode, comment);
return true;
}
return false;
}
function handleForComments({ comment, enclosingNode }) {
if (
enclosingNode &&
(enclosingNode.type === "ForInStatement" ||
enclosingNode.type === "ForOfStatement")
) {
addLeadingComment(enclosingNode, comment);
return true;
}
return false;
}
function handleImportDeclarationComments({
comment,
precedingNode,
enclosingNode,
text,
}) {
if (
precedingNode &&
precedingNode.type === "ImportSpecifier" &&
enclosingNode &&
enclosingNode.type === "ImportDeclaration" &&
hasNewline(text, locEnd(comment))
) {
addTrailingComment(precedingNode, comment);
return true;
}
return false;
}
function handleAssignmentPatternComments({ comment, enclosingNode }) {
if (enclosingNode && enclosingNode.type === "AssignmentPattern") {
addLeadingComment(enclosingNode, comment);
return true;
}
return false;
}
function handleTypeAliasComments({ comment, enclosingNode }) {
if (enclosingNode && enclosingNode.type === "TypeAlias") {
addLeadingComment(enclosingNode, comment);
return true;
}
return false;
}
function handleVariableDeclaratorComments({
comment,
enclosingNode,
followingNode,
}) {
if (
enclosingNode &&
(enclosingNode.type === "VariableDeclarator" ||
enclosingNode.type === "AssignmentExpression") &&
followingNode &&
(followingNode.type === "ObjectExpression" ||
followingNode.type === "ArrayExpression" ||
followingNode.type === "TemplateLiteral" ||
followingNode.type === "TaggedTemplateExpression" ||
isBlockComment(comment))
) {
addLeadingComment(followingNode, comment);
return true;
}
return false;
}
function handleTSFunctionTrailingComments({
comment,
enclosingNode,
followingNode,
text,
}) {
if (
!followingNode &&
enclosingNode &&
(enclosingNode.type === "TSMethodSignature" ||
enclosingNode.type === "TSDeclareFunction" ||
enclosingNode.type === "TSAbstractMethodDefinition") &&
getNextNonSpaceNonCommentCharacter(text, comment, locEnd) === ";"
) {
addTrailingComment(enclosingNode, comment);
return true;
}
return false;
}
function handleIgnoreComments({ comment, enclosingNode, followingNode }) {
if (
isPrettierIgnoreComment(comment) &&
enclosingNode &&
enclosingNode.type === "TSMappedType" &&
followingNode &&
followingNode.type === "TSTypeParameter" &&
followingNode.constraint
) {
enclosingNode.prettierIgnore = true;
comment.unignore = true;
return true;
}
}
function handleTSMappedTypeComments({
comment,
precedingNode,
enclosingNode,
followingNode,
}) {
if (!enclosingNode || enclosingNode.type !== "TSMappedType") {
return false;
}
if (
followingNode &&
followingNode.type === "TSTypeParameter" &&
followingNode.name
) {
addLeadingComment(followingNode.name, comment);
return true;
}
if (
precedingNode &&
precedingNode.type === "TSTypeParameter" &&
precedingNode.constraint
) {
addTrailingComment(precedingNode.constraint, comment);
return true;
}
return false;
}
/**
* @param {Node} node
* @returns {boolean}
*/
function isRealFunctionLikeNode(node) {
return (
node.type === "ArrowFunctionExpression" ||
node.type === "FunctionExpression" ||
node.type === "FunctionDeclaration" ||
node.type === "ObjectMethod" ||
node.type === "ClassMethod" ||
node.type === "TSDeclareFunction" ||
node.type === "TSCallSignatureDeclaration" ||
node.type === "TSConstructSignatureDeclaration" ||
node.type === "TSMethodSignature" ||
node.type === "TSConstructorType" ||
node.type === "TSFunctionType" ||
node.type === "TSDeclareMethod"
);
}
/**
* @param {any} node
* @returns {Node[] | void}
*/
function getCommentChildNodes(node, options) {
// Prevent attaching comments to FunctionExpression in this case:
// class Foo {
// bar() // comment
// {
// baz();
// }
// }
if (
(options.parser === "typescript" ||
// [prettierx] support __typescript_estree parser option for testing
options.parser === "__typescript_estree" ||
options.parser === "flow" ||
options.parser === "espree" ||
options.parser === "meriyah" ||
options.parser === "__babel_estree") &&
node.type === "MethodDefinition" &&
node.value &&
node.value.type === "FunctionExpression" &&
getFunctionParameters(node.value).length === 0 &&
!node.value.returnType &&
!isNonEmptyArray(node.value.typeParameters) &&
node.value.body
) {
return [...(node.decorators || []), node.key, node.value.body];
}
}
/**
* @param {Comment} comment
* @returns {boolean}
*/
function isTypeCastComment(comment) {
return (
isBlockComment(comment) &&
comment.value[0] === "*" &&
// TypeScript expects the type to be enclosed in curly brackets, however
// Closure Compiler accepts types in parens and even without any delimiters at all.
// That's why we just search for "@type".
/\b/.test(comment.value)
);
}
/**
* @param {AstPath} path
* @returns {boolean}
*/
function willPrintOwnComments(path /*, options */) {
const node = path.getValue();
const parent = path.getParentNode();
return (
((node &&
(isJsxNode(node) ||
hasFlowShorthandAnnotationComment(node) ||
(isCallExpression(parent) &&
(hasFlowAnnotationComment(node.leadingComments) ||
hasFlowAnnotationComment(node.trailingComments))))) ||
(parent &&
(parent.type === "JSXSpreadAttribute" ||
parent.type === "JSXSpreadChild" ||
parent.type === "UnionTypeAnnotation" ||
parent.type === "TSUnionType" ||
((parent.type === "ClassDeclaration" ||
parent.type === "ClassExpression") &&
parent.superClass === node)))) &&
(!hasIgnoreComment(path) ||
parent.type === "UnionTypeAnnotation" ||
parent.type === "TSUnionType")
);
}
module.exports = {
handleOwnLineComment,
handleEndOfLineComment,
handleRemainingComment,
isTypeCastComment,
getCommentChildNodes,
willPrintOwnComments,
};