diff --git a/.eslintrc.yml b/.eslintrc.yml index d7688400b..d0e2c6b1a 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -109,6 +109,7 @@ overrides: - files: src/**/*.js rules: prettier-internal-rules/prefer-fast-path-each: error + prettier-internal-rules/prefer-is-non-empty-array: error - files: - src/language-*/**/*.js rules: diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/index.js b/scripts/tools/eslint-plugin-prettier-internal-rules/index.js index 3dffdeef9..9837c5ae5 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/index.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/index.js @@ -7,6 +7,7 @@ module.exports = { "jsx-identifier-case": require("./jsx-identifier-case"), "no-node-comments": require("./no-node-comments"), "prefer-fast-path-each": require("./prefer-fast-path-each"), + "prefer-is-non-empty-array": require("./prefer-is-non-empty-array"), "require-json-extensions": require("./require-json-extensions"), }, }; diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-is-non-empty-array.js b/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-is-non-empty-array.js new file mode 100644 index 000000000..81fe8932e --- /dev/null +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-is-non-empty-array.js @@ -0,0 +1,152 @@ +"use strict"; + +const getLengthSelector = (path) => + `[${path}.type="MemberExpression"][${path}.property.type="Identifier"][${path}.property.name="length"]`; +const selector = [ + "LogicalExpression", + ':not(FunctionDeclaration[id.name="isNonEmptyArray"] *)', + '[operator="&&"]', + `:matches(${[ + // `&& foo.length` + getLengthSelector("right"), + // `&& foo.length !== 0` + // `&& foo.length > 0` + [ + '[right.type="BinaryExpression"]', + ':matches([right.operator="!=="], [right.operator=">"])', + getLengthSelector("right.left"), + '[right.right.type="Literal"]', + '[right.right.raw="0"]', + ].join(""), + ].join(", ")})`, +].join(""); + +const negativeSelector = [ + "LogicalExpression", + '[operator="||"]', + `:matches(${[ + // `|| !foo.length` + [ + '[right.type="UnaryExpression"]', + '[right.operator="!"]', + getLengthSelector("right.argument"), + ].join(""), + // `|| foo.length === 0` + [ + '[right.type="BinaryExpression"]', + '[right.operator="==="]', + getLengthSelector("right.left"), + '[right.right.type="Literal"]', + '[right.right.raw="0"]', + ].join(""), + ].join(", ")})`, +].join(""); + +const isArrayIsArrayCall = (node) => + node.type === "CallExpression" && + node.callee.type === "MemberExpression" && + node.callee.object.type === "Identifier" && + node.callee.object.name === "Array" && + node.callee.property.type === "Identifier" && + node.callee.property.name === "isArray"; + +const MESSAGE_ID = "prefer-is-non-empty-array"; + +module.exports = { + meta: { + type: "suggestion", + docs: { + url: + "https://github.com/prettier/prettier/blob/master/scripts/eslint-plugin-prettier-internal-rules/prefer-is-non-empty-array.js", + }, + messages: { + [MESSAGE_ID]: "Please use `isNonEmptyArray()`.", + }, + fixable: "code", + }, + create(context) { + const sourceCode = context.getSourceCode(); + + return { + [selector](node) { + let { left, right } = node; + + while (left.type === "LogicalExpression" && left.operator === "&&") { + left = left.right; + } + + let leftObject = left; + // `Array.isArray(foo)` + if (isArrayIsArrayCall(leftObject)) { + leftObject = leftObject.arguments[0]; + } + + const rightObject = + right.type === "BinaryExpression" ? right.left.object : right.object; + const objectText = sourceCode.getText(rightObject); + // Simple compare with code + if (sourceCode.getText(leftObject) !== objectText) { + return; + } + + const [start] = left.range; + const [, end] = node.range; + context.report({ + loc: { + start: sourceCode.getLocFromIndex(start), + end: sourceCode.getLocFromIndex(end), + }, + messageId: MESSAGE_ID, + fix(fixer) { + return fixer.replaceTextRange( + [start, end], + `isNonEmptyArray(${objectText})` + ); + }, + }); + }, + [negativeSelector](node) { + let { left, right } = node; + + while (left.type === "LogicalExpression" && left.operator === "||") { + left = left.right; + } + + if (!left.type === "UnaryExpression" || left.operator !== "!") { + return; + } + + const rightObject = + right.type === "UnaryExpression" + ? right.argument.object + : right.left.object; + let leftObject = left.argument; + if (isArrayIsArrayCall(leftObject)) { + leftObject = leftObject.arguments[0]; + } + + const objectText = sourceCode.getText(rightObject); + // Simple compare with code + if (sourceCode.getText(leftObject) !== objectText) { + return; + } + + const [start] = left.range; + const [, end] = node.range; + context.report({ + loc: { + start: sourceCode.getLocFromIndex(start), + end: sourceCode.getLocFromIndex(end), + }, + messageId: MESSAGE_ID, + fix(fixer) { + return fixer.replaceTextRange( + [start, end], + `!isNonEmptyArray(${objectText})` + ); + }, + }); + }, + }; + }, +}; diff --git a/src/common/util.js b/src/common/util.js index 50d33bfb1..e6d6225a8 100644 --- a/src/common/util.js +++ b/src/common/util.js @@ -624,6 +624,10 @@ function getShebang(text) { return text.slice(0, index); } +function isNonEmptyArray(object) { + return Array.isArray(object) && object.length > 0; +} + module.exports = { inferParserByLanguage, replaceEndOfLineWith, @@ -660,4 +664,5 @@ module.exports = { addTrailingComment, isFrontMatterNode, getShebang, + isNonEmptyArray, }; diff --git a/src/language-css/printer-postcss.js b/src/language-css/printer-postcss.js index bbbf0c3e6..346b822ae 100644 --- a/src/language-css/printer-postcss.js +++ b/src/language-css/printer-postcss.js @@ -6,6 +6,7 @@ const { hasNewline, isFrontMatterNode, isNextLineEmpty, + isNonEmptyArray, } = require("../common/util"); const { builders: { @@ -469,7 +470,7 @@ function genericPrint(path, options, print) { case "selector-pseudo": { return concat([ maybeToLowerCase(node.value), - node.nodes && node.nodes.length > 0 + isNonEmptyArray(node.nodes) ? concat(["(", join(", ", path.map(print, "nodes")), ")"]) : "", ]); diff --git a/src/language-css/utils.js b/src/language-css/utils.js index 7cd711416..20e1bff38 100644 --- a/src/language-css/utils.js +++ b/src/language-css/utils.js @@ -1,5 +1,6 @@ "use strict"; +const { isNonEmptyArray } = require("../common/util"); const colorAdjusterFunctions = new Set([ "red", "green", @@ -59,7 +60,7 @@ function getPropOfDeclNode(path) { } function hasSCSSInterpolation(groupList) { - if (groupList && groupList.length) { + if (isNonEmptyArray(groupList)) { for (let i = groupList.length - 1; i > 0; i--) { // If we find `#{`, return true. if ( @@ -76,7 +77,7 @@ function hasSCSSInterpolation(groupList) { } function hasStringOrFunction(groupList) { - if (groupList && groupList.length) { + if (isNonEmptyArray(groupList)) { for (let i = 0; i < groupList.length; i++) { if (groupList[i].type === "string" || groupList[i].type === "func") { return true; diff --git a/src/language-graphql/printer-graphql.js b/src/language-graphql/printer-graphql.js index 28cc63a8f..f12a86285 100644 --- a/src/language-graphql/printer-graphql.js +++ b/src/language-graphql/printer-graphql.js @@ -3,7 +3,7 @@ const { builders: { concat, join, hardline, line, softline, group, indent, ifBreak }, } = require("../document"); -const { isNextLineEmpty } = require("../common/util"); +const { isNextLineEmpty, isNonEmptyArray } = require("../common/util"); const { insertPragma } = require("./pragma"); const { locStart, locEnd } = require("./loc"); @@ -39,7 +39,7 @@ function genericPrint(path, options, print) { return concat([ hasOperation ? n.operation : "", hasOperation && hasName ? concat([" ", path.call(print, "name")]) : "", - n.variableDefinitions && n.variableDefinitions.length + isNonEmptyArray(n.variableDefinitions) ? group( concat([ "(", @@ -66,7 +66,7 @@ function genericPrint(path, options, print) { return concat([ "fragment ", path.call(print, "name"), - n.variableDefinitions && n.variableDefinitions.length + isNonEmptyArray(n.variableDefinitions) ? group( concat([ "(", diff --git a/src/language-html/ast.js b/src/language-html/ast.js index 85279d5c2..f612f67be 100644 --- a/src/language-html/ast.js +++ b/src/language-html/ast.js @@ -1,5 +1,6 @@ "use strict"; +const { isNonEmptyArray } = require("../common/util"); const NODES_KEYS = { attrs: true, children: true, @@ -71,13 +72,11 @@ class Node { } get firstChild() { - return this.children && this.children.length !== 0 - ? this.children[0] - : null; + return isNonEmptyArray(this.children) ? this.children[0] : null; } get lastChild() { - return this.children && this.children.length !== 0 + return isNonEmptyArray(this.children) ? this.children[this.children.length - 1] : null; } diff --git a/src/language-html/printer-html.js b/src/language-html/printer-html.js index c00ee53f4..9110cfde0 100644 --- a/src/language-html/printer-html.js +++ b/src/language-html/printer-html.js @@ -18,7 +18,7 @@ const { }, utils: { mapDoc, cleanDoc }, } = require("../document"); -const { replaceEndOfLineWith } = require("../common/util"); +const { replaceEndOfLineWith, isNonEmptyArray } = require("../common/util"); const { print: printFrontMatter } = require("../utils/front-matter"); const clean = require("./clean"); const { @@ -657,7 +657,7 @@ function getNodeContent(node, options) { function printAttributes(path, options, print) { const node = path.getValue(); - if (!node.attrs || node.attrs.length === 0) { + if (!isNonEmptyArray(node.attrs)) { return node.isSelfClosing ? /** *
diff --git a/src/language-js/comments.js b/src/language-js/comments.js index 128026d41..fdf2fda37 100644 --- a/src/language-js/comments.js +++ b/src/language-js/comments.js @@ -10,6 +10,7 @@ const { addTrailingComment, addDanglingComment, getNextNonSpaceNonCommentCharacterIndex, + isNonEmptyArray, } = require("../common/util"); const { isBlockComment, @@ -386,8 +387,7 @@ function handleClassComments({ enclosingNode.type === "TSInterfaceDeclaration") ) { if ( - enclosingNode.decorators && - enclosingNode.decorators.length > 0 && + isNonEmptyArray(enclosingNode.decorators) && !(followingNode && followingNode.type === "Decorator") ) { addTrailingComment( @@ -924,7 +924,7 @@ function getCommentChildNodes(node, options) { node.value.type === "FunctionExpression" && getFunctionParameters(node.value).length === 0 && !node.value.returnType && - (!node.value.typeParameters || node.value.typeParameters.length === 0) && + !isNonEmptyArray(node.value.typeParameters) && node.value.body ) { return [...(node.decorators || []), node.key, node.value.body]; diff --git a/src/language-js/embed/css.js b/src/language-js/embed/css.js index cfd25cece..13ddda499 100644 --- a/src/language-js/embed/css.js +++ b/src/language-js/embed/css.js @@ -1,5 +1,6 @@ "use strict"; +const { isNonEmptyArray } = require("../../common/util"); const { builders: { indent, hardline, softline, concat }, utils: { mapDoc, replaceNewlinesWithLiterallines, cleanDoc }, @@ -52,7 +53,7 @@ function transformCssDoc(quasisDoc, parentNode, expressionDocs) { // returns a new doc with all the placeholders replaced, // or null if it couldn't replace any expression function replacePlaceholders(quasisDoc, expressionDocs) { - if (!expressionDocs || !expressionDocs.length) { + if (!isNonEmptyArray(expressionDocs)) { return quasisDoc; } let replaceCounter = 0; diff --git a/src/language-js/loc.js b/src/language-js/loc.js index d6409ca92..68b020010 100644 --- a/src/language-js/loc.js +++ b/src/language-js/loc.js @@ -1,5 +1,7 @@ "use strict"; +const { isNonEmptyArray } = require("../common/util"); + /** * @typedef {import("./types/estree").Node} Node */ @@ -12,7 +14,7 @@ function locStart(node, opts) { const decorators = (node.declaration && node.declaration.decorators) || node.decorators; - if (decorators && decorators.length > 0) { + if (isNonEmptyArray(decorators)) { return locStart(decorators[0]); } } diff --git a/src/language-js/print/assignment.js b/src/language-js/print/assignment.js index 5088c610a..8155f2d40 100644 --- a/src/language-js/print/assignment.js +++ b/src/language-js/print/assignment.js @@ -1,5 +1,6 @@ "use strict"; +const { isNonEmptyArray } = require("../../common/util"); const { builders: { concat, line, group, indent }, } = require("../../document"); @@ -69,8 +70,7 @@ function printAssignmentRight(leftNode, rightNode, printedRight, options) { !shouldInlineLogicalExpression(rightNode.test)) || rightNode.type === "StringLiteralTypeAnnotation" || (rightNode.type === "ClassExpression" && - rightNode.decorators && - rightNode.decorators.length) || + isNonEmptyArray(rightNode.decorators)) || ((leftNode.type === "Identifier" || isStringLiteral(leftNode) || leftNode.type === "MemberExpression") && diff --git a/src/language-js/print/block.js b/src/language-js/print/block.js index 694501fc8..057ad9634 100644 --- a/src/language-js/print/block.js +++ b/src/language-js/print/block.js @@ -1,7 +1,7 @@ "use strict"; const { printDanglingComments } = require("../../main/comments"); -const { isNextLineEmpty } = require("../../common/util"); +const { isNextLineEmpty, isNonEmptyArray } = require("../../common/util"); const { builders: { concat, hardline, indent }, } = require("../../document"); @@ -21,7 +21,6 @@ function printBlock(path, options, print) { } parts.push("{"); - const printed = printBlockBody(path, options, print); if (printed) { parts.push(indent(concat([hardline, printed])), hardline); @@ -58,8 +57,7 @@ function printBlock(path, options, print) { function printBlockBody(path, options, print) { const node = path.getValue(); - const nodeHasDirectives = - Array.isArray(node.directives) && node.directives.length > 0; + const nodeHasDirectives = isNonEmptyArray(node.directives); const nodeHasBody = node.body.some((node) => node.type !== "EmptyStatement"); const nodeHasComment = hasComment(node, CommentCheckFlags.Dangling); diff --git a/src/language-js/print/class.js b/src/language-js/print/class.js index b0e588c54..d8169cf10 100644 --- a/src/language-js/print/class.js +++ b/src/language-js/print/class.js @@ -1,5 +1,6 @@ "use strict"; +const { isNonEmptyArray } = require("../../common/util"); const { printComments, printDanglingComments } = require("../../main/comments"); const { builders: { concat, join, line, hardline, softline, group, indent, ifBreak }, @@ -35,9 +36,9 @@ function printClass(path, options, print) { const groupMode = (n.id && hasComment(n.id, CommentCheckFlags.Trailing)) || (n.superClass && hasComment(n.superClass)) || - (n.extends && n.extends.length !== 0) || // DeclareClass - (n.mixins && n.mixins.length !== 0) || - (n.implements && n.implements.length !== 0); + isNonEmptyArray(n.extends) || // DeclareClass + isNonEmptyArray(n.mixins) || + isNonEmptyArray(n.implements); const partsGroup = []; const extendsParts = []; @@ -113,7 +114,7 @@ function shouldIndentOnlyHeritageClauses(node) { function printList(path, options, print, listName) { const n = path.getValue(); - if (!n[listName] || n[listName].length === 0) { + if (!isNonEmptyArray(n[listName])) { return ""; } @@ -158,7 +159,7 @@ function printClassMethod(path, options, print) { const n = path.getValue(); const parts = []; - if (n.decorators && n.decorators.length !== 0) { + if (isNonEmptyArray(n.decorators)) { parts.push(printDecorators(path, options, print)); } if (n.accessibility) { @@ -197,7 +198,7 @@ function printClassProperty(path, options, print) { const parts = []; const semi = options.semi ? ";" : ""; - if (n.decorators && n.decorators.length !== 0) { + if (isNonEmptyArray(n.decorators)) { parts.push(printDecorators(path, options, print)); } if (n.accessibility) { diff --git a/src/language-js/print/interface.js b/src/language-js/print/interface.js index 84f42657e..68e7d07e3 100644 --- a/src/language-js/print/interface.js +++ b/src/language-js/print/interface.js @@ -1,5 +1,6 @@ "use strict"; +const { isNonEmptyArray } = require("../../common/util"); const { builders: { concat, join, line, group, indent, ifBreak }, } = require("../../document"); @@ -41,7 +42,7 @@ function printInterface(path, options, print) { CommentCheckFlags.Trailing | CommentCheckFlags.Line ); - if (n.extends && n.extends.length !== 0) { + if (isNonEmptyArray(n.extends)) { extendsParts.push( shouldIndentOnlyHeritageClauses ? ifBreak(" ", line, { @@ -57,7 +58,7 @@ function printInterface(path, options, print) { if ( (n.id && hasComment(n.id, CommentCheckFlags.Trailing)) || - (n.extends && n.extends.length !== 0) + isNonEmptyArray(n.extends) ) { const printedExtends = concat(extendsParts); if (shouldIndentOnlyHeritageClauses) { diff --git a/src/language-js/print/misc.js b/src/language-js/print/misc.js index 8623edb6a..892365c5f 100644 --- a/src/language-js/print/misc.js +++ b/src/language-js/print/misc.js @@ -1,5 +1,6 @@ "use strict"; +const { isNonEmptyArray } = require("../../common/util"); const { builders: { concat, indent, join, line }, } = require("../../document"); @@ -67,7 +68,7 @@ function printBindExpressionCallee(path, options, print) { function printTypeScriptModifiers(path, options, print) { const n = path.getValue(); - if (!n.modifiers || !n.modifiers.length) { + if (!isNonEmptyArray(n.modifiers)) { return ""; } return concat([join(" ", path.map(print, "modifiers")), " "]); diff --git a/src/language-js/print/module.js b/src/language-js/print/module.js index af0de27b9..567c6cb54 100644 --- a/src/language-js/print/module.js +++ b/src/language-js/print/module.js @@ -1,5 +1,6 @@ "use strict"; +const { isNonEmptyArray } = require("../../common/util"); const { builders: { concat, softline, group, indent, join, line, ifBreak, hardline }, } = require("../../document"); @@ -168,7 +169,7 @@ function printModuleSpecifiers(path, options, print) { /** @type{Doc[]} */ const parts = [" "]; - if (node.specifiers && node.specifiers.length > 0) { + if (isNonEmptyArray(node.specifiers)) { const standaloneSpecifiers = []; const groupedSpecifiers = []; @@ -246,7 +247,7 @@ function shouldNotPrintSpecifiers(node, options) { if ( type !== "ImportDeclaration" || - (Array.isArray(specifiers) && specifiers.length > 0) || + isNonEmptyArray(specifiers) || importKind === "type" ) { return false; @@ -260,7 +261,7 @@ function shouldNotPrintSpecifiers(node, options) { function printImportAssertions(path, options, print) { const node = path.getNode(); - if (Array.isArray(node.assertions) && node.assertions.length !== 0) { + if (isNonEmptyArray(node.assertions)) { return concat([ " assert {", options.bracketSpacing ? " " : "", diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js index 32f0abd40..8102d22a7 100644 --- a/src/language-js/printer-estree.js +++ b/src/language-js/printer-estree.js @@ -12,6 +12,7 @@ const { printString, printNumber, isNextLineEmpty, + isNonEmptyArray, } = require("../common/util"); const { builders: { @@ -127,8 +128,7 @@ function genericPrint(path, options, printPath, args) { ) { // their decorators are handled themselves } else if ( - node.decorators && - node.decorators.length > 0 && + isNonEmptyArray(node.decorators) && // If the parent node is an export declaration and the decorator // was written before the export, the export will be responsible // for printing the decorators. @@ -162,8 +162,7 @@ function genericPrint(path, options, printPath, args) { } else if ( isExportDeclaration(node) && node.declaration && - node.declaration.decorators && - node.declaration.decorators.length > 0 && + isNonEmptyArray(node.declaration.decorators) && // Only print decorators here if they were written before the export, // otherwise they are printed by the node.declaration locStart(node, { ignoreDecorators: true }) > diff --git a/src/language-js/utils.js b/src/language-js/utils.js index b3b0bb927..0f49e57eb 100644 --- a/src/language-js/utils.js +++ b/src/language-js/utils.js @@ -6,6 +6,7 @@ const { hasNewline, hasNewlineInRange, skipWhitespace, + isNonEmptyArray, } = require("../common/util"); const { locStart, locEnd, hasSameLocStart } = require("./loc"); @@ -1470,7 +1471,7 @@ const getCommentTestFunction = (flags, fn) => { * @returns {boolean} */ function hasComment(node, flags, fn) { - if (!node || !Array.isArray(node.comments) || node.comments.length === 0) { + if (!node || !isNonEmptyArray(node.comments)) { return false; } const test = getCommentTestFunction(flags, fn); diff --git a/src/language-markdown/printer-markdown.js b/src/language-markdown/printer-markdown.js index d55d1bb83..82af78a16 100644 --- a/src/language-markdown/printer-markdown.js +++ b/src/language-markdown/printer-markdown.js @@ -5,6 +5,7 @@ const { getMinNotPresentContinuousCount, getMaxContinuousCount, getStringWidth, + isNonEmptyArray, } = require("../common/util"); const { builders: { @@ -773,7 +774,7 @@ function printChildren(path, options, print, events) { function getLastDescendantNode(node) { let current = node; - while (current.children && current.children.length !== 0) { + while (isNonEmptyArray(current.children)) { current = current.children[current.children.length - 1]; } return current; diff --git a/src/language-yaml/utils.js b/src/language-yaml/utils.js index 615be327e..5583ef694 100644 --- a/src/language-yaml/utils.js +++ b/src/language-yaml/utils.js @@ -1,6 +1,6 @@ "use strict"; -const { getLast } = require("../common/util"); +const { getLast, isNonEmptyArray } = require("../common/util"); function getAncestorCount(path, filter) { let counter = 0; @@ -98,7 +98,7 @@ function isLastDescendantNode(path) { } function getLastDescendantNode(node) { - return "children" in node && node.children.length !== 0 + return isNonEmptyArray(node.children) ? getLastDescendantNode(getLast(node.children)) : node; } @@ -124,7 +124,7 @@ function hasPrettierIgnore(path) { } function isEmptyNode(node) { - return (!node.children || node.children.length === 0) && !hasComments(node); + return !isNonEmptyArray(node.children) && !hasComments(node); } function hasComments(node) { @@ -138,11 +138,11 @@ function hasComments(node) { } function hasLeadingComments(node) { - return node && node.leadingComments && node.leadingComments.length !== 0; + return node && isNonEmptyArray(node.leadingComments); } function hasMiddleComments(node) { - return node && node.middleComments && node.middleComments.length !== 0; + return node && isNonEmptyArray(node.middleComments); } function hasIndicatorComment(node) { @@ -154,7 +154,7 @@ function hasTrailingComment(node) { } function hasEndComments(node) { - return node && node.endComments && node.endComments.length !== 0; + return node && isNonEmptyArray(node.endComments); } /** diff --git a/src/main/comments.js b/src/main/comments.js index a7ee5ba70..8482780c2 100644 --- a/src/main/comments.js +++ b/src/main/comments.js @@ -24,6 +24,7 @@ const { addLeadingComment, addDanglingComment, addTrailingComment, + isNonEmptyArray, } = require("../common/util"); const childNodesCache = new WeakMap(); @@ -559,7 +560,7 @@ function printComments(path, print, options, needsSemi) { const printed = print(path); const comments = value && value.comments; - if (!comments || comments.length === 0) { + if (!isNonEmptyArray(comments)) { return prependCursorPlaceholder(path, options, printed); }