1/** 2 * @fileoverview Flag expressions in statement position that do not side effect 3 * @author Michael Ficarra 4 */ 5"use strict"; 6 7//------------------------------------------------------------------------------ 8// Rule Definition 9//------------------------------------------------------------------------------ 10 11/** 12 * Returns `true`. 13 * @returns {boolean} `true`. 14 */ 15function alwaysTrue() { 16 return true; 17} 18 19/** 20 * Returns `false`. 21 * @returns {boolean} `false`. 22 */ 23function alwaysFalse() { 24 return false; 25} 26 27module.exports = { 28 meta: { 29 type: "suggestion", 30 31 docs: { 32 description: "disallow unused expressions", 33 category: "Best Practices", 34 recommended: false, 35 url: "https://eslint.org/docs/rules/no-unused-expressions" 36 }, 37 38 schema: [ 39 { 40 type: "object", 41 properties: { 42 allowShortCircuit: { 43 type: "boolean", 44 default: false 45 }, 46 allowTernary: { 47 type: "boolean", 48 default: false 49 }, 50 allowTaggedTemplates: { 51 type: "boolean", 52 default: false 53 } 54 }, 55 additionalProperties: false 56 } 57 ], 58 59 messages: { 60 unusedExpression: "Expected an assignment or function call and instead saw an expression." 61 } 62 }, 63 64 create(context) { 65 const config = context.options[0] || {}, 66 allowShortCircuit = config.allowShortCircuit || false, 67 allowTernary = config.allowTernary || false, 68 allowTaggedTemplates = config.allowTaggedTemplates || false; 69 70 // eslint-disable-next-line jsdoc/require-description 71 /** 72 * @param {ASTNode} node any node 73 * @returns {boolean} whether the given node structurally represents a directive 74 */ 75 function looksLikeDirective(node) { 76 return node.type === "ExpressionStatement" && 77 node.expression.type === "Literal" && typeof node.expression.value === "string"; 78 } 79 80 // eslint-disable-next-line jsdoc/require-description 81 /** 82 * @param {Function} predicate ([a] -> Boolean) the function used to make the determination 83 * @param {a[]} list the input list 84 * @returns {a[]} the leading sequence of members in the given list that pass the given predicate 85 */ 86 function takeWhile(predicate, list) { 87 for (let i = 0; i < list.length; ++i) { 88 if (!predicate(list[i])) { 89 return list.slice(0, i); 90 } 91 } 92 return list.slice(); 93 } 94 95 // eslint-disable-next-line jsdoc/require-description 96 /** 97 * @param {ASTNode} node a Program or BlockStatement node 98 * @returns {ASTNode[]} the leading sequence of directive nodes in the given node's body 99 */ 100 function directives(node) { 101 return takeWhile(looksLikeDirective, node.body); 102 } 103 104 // eslint-disable-next-line jsdoc/require-description 105 /** 106 * @param {ASTNode} node any node 107 * @param {ASTNode[]} ancestors the given node's ancestors 108 * @returns {boolean} whether the given node is considered a directive in its current position 109 */ 110 function isDirective(node, ancestors) { 111 const parent = ancestors[ancestors.length - 1], 112 grandparent = ancestors[ancestors.length - 2]; 113 114 return (parent.type === "Program" || parent.type === "BlockStatement" && 115 (/Function/u.test(grandparent.type))) && 116 directives(parent).indexOf(node) >= 0; 117 } 118 119 /** 120 * The member functions return `true` if the type has no side-effects. 121 * Unknown nodes are handled as `false`, then this rule ignores those. 122 */ 123 const Checker = Object.assign(Object.create(null), { 124 isDisallowed(node) { 125 return (Checker[node.type] || alwaysFalse)(node); 126 }, 127 128 ArrayExpression: alwaysTrue, 129 ArrowFunctionExpression: alwaysTrue, 130 BinaryExpression: alwaysTrue, 131 ChainExpression(node) { 132 return Checker.isDisallowed(node.expression); 133 }, 134 ClassExpression: alwaysTrue, 135 ConditionalExpression(node) { 136 if (allowTernary) { 137 return Checker.isDisallowed(node.consequent) || Checker.isDisallowed(node.alternate); 138 } 139 return true; 140 }, 141 FunctionExpression: alwaysTrue, 142 Identifier: alwaysTrue, 143 Literal: alwaysTrue, 144 LogicalExpression(node) { 145 if (allowShortCircuit) { 146 return Checker.isDisallowed(node.right); 147 } 148 return true; 149 }, 150 MemberExpression: alwaysTrue, 151 MetaProperty: alwaysTrue, 152 ObjectExpression: alwaysTrue, 153 SequenceExpression: alwaysTrue, 154 TaggedTemplateExpression() { 155 return !allowTaggedTemplates; 156 }, 157 TemplateLiteral: alwaysTrue, 158 ThisExpression: alwaysTrue, 159 UnaryExpression(node) { 160 return node.operator !== "void" && node.operator !== "delete"; 161 } 162 }); 163 164 return { 165 ExpressionStatement(node) { 166 if (Checker.isDisallowed(node.expression) && !isDirective(node, context.getAncestors())) { 167 context.report({ node, messageId: "unusedExpression" }); 168 } 169 } 170 }; 171 } 172}; 173