• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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