]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
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 | module.exports = { | |
12 | meta: { | |
13 | type: "suggestion", | |
14 | ||
15 | docs: { | |
16 | description: "disallow unused expressions", | |
17 | category: "Best Practices", | |
18 | recommended: false, | |
19 | url: "https://eslint.org/docs/rules/no-unused-expressions" | |
20 | }, | |
21 | ||
22 | schema: [ | |
23 | { | |
24 | type: "object", | |
25 | properties: { | |
26 | allowShortCircuit: { | |
27 | type: "boolean", | |
28 | default: false | |
29 | }, | |
30 | allowTernary: { | |
31 | type: "boolean", | |
32 | default: false | |
33 | }, | |
34 | allowTaggedTemplates: { | |
35 | type: "boolean", | |
36 | default: false | |
37 | } | |
38 | }, | |
39 | additionalProperties: false | |
40 | } | |
41 | ], | |
42 | ||
43 | messages: { | |
44 | unusedExpression: "Expected an assignment or function call and instead saw an expression." | |
45 | } | |
46 | }, | |
47 | ||
48 | create(context) { | |
49 | const config = context.options[0] || {}, | |
50 | allowShortCircuit = config.allowShortCircuit || false, | |
51 | allowTernary = config.allowTernary || false, | |
52 | allowTaggedTemplates = config.allowTaggedTemplates || false; | |
53 | ||
54 | // eslint-disable-next-line jsdoc/require-description | |
55 | /** | |
56 | * @param {ASTNode} node any node | |
57 | * @returns {boolean} whether the given node structurally represents a directive | |
58 | */ | |
59 | function looksLikeDirective(node) { | |
60 | return node.type === "ExpressionStatement" && | |
61 | node.expression.type === "Literal" && typeof node.expression.value === "string"; | |
62 | } | |
63 | ||
64 | // eslint-disable-next-line jsdoc/require-description | |
65 | /** | |
66 | * @param {Function} predicate ([a] -> Boolean) the function used to make the determination | |
67 | * @param {a[]} list the input list | |
68 | * @returns {a[]} the leading sequence of members in the given list that pass the given predicate | |
69 | */ | |
70 | function takeWhile(predicate, list) { | |
71 | for (let i = 0; i < list.length; ++i) { | |
72 | if (!predicate(list[i])) { | |
73 | return list.slice(0, i); | |
74 | } | |
75 | } | |
76 | return list.slice(); | |
77 | } | |
78 | ||
79 | // eslint-disable-next-line jsdoc/require-description | |
80 | /** | |
81 | * @param {ASTNode} node a Program or BlockStatement node | |
82 | * @returns {ASTNode[]} the leading sequence of directive nodes in the given node's body | |
83 | */ | |
84 | function directives(node) { | |
85 | return takeWhile(looksLikeDirective, node.body); | |
86 | } | |
87 | ||
88 | // eslint-disable-next-line jsdoc/require-description | |
89 | /** | |
90 | * @param {ASTNode} node any node | |
91 | * @param {ASTNode[]} ancestors the given node's ancestors | |
92 | * @returns {boolean} whether the given node is considered a directive in its current position | |
93 | */ | |
94 | function isDirective(node, ancestors) { | |
95 | const parent = ancestors[ancestors.length - 1], | |
96 | grandparent = ancestors[ancestors.length - 2]; | |
97 | ||
98 | return (parent.type === "Program" || parent.type === "BlockStatement" && | |
99 | (/Function/u.test(grandparent.type))) && | |
100 | directives(parent).indexOf(node) >= 0; | |
101 | } | |
102 | ||
103 | /** | |
104 | * Determines whether or not a given node is a valid expression. Recurses on short circuit eval and ternary nodes if enabled by flags. | |
105 | * @param {ASTNode} node any node | |
106 | * @returns {boolean} whether the given node is a valid expression | |
107 | */ | |
108 | function isValidExpression(node) { | |
109 | if (allowTernary) { | |
110 | ||
111 | // Recursive check for ternary and logical expressions | |
112 | if (node.type === "ConditionalExpression") { | |
113 | return isValidExpression(node.consequent) && isValidExpression(node.alternate); | |
114 | } | |
115 | } | |
116 | ||
117 | if (allowShortCircuit) { | |
118 | if (node.type === "LogicalExpression") { | |
119 | return isValidExpression(node.right); | |
120 | } | |
121 | } | |
122 | ||
123 | if (allowTaggedTemplates && node.type === "TaggedTemplateExpression") { | |
124 | return true; | |
125 | } | |
126 | ||
d3726936 | 127 | return /^(?:Assignment|Call|New|Update|Yield|Await|Import)Expression$/u.test(node.type) || |
eb39fafa DC |
128 | (node.type === "UnaryExpression" && ["delete", "void"].indexOf(node.operator) >= 0); |
129 | } | |
130 | ||
131 | return { | |
132 | ExpressionStatement(node) { | |
133 | if (!isValidExpression(node.expression) && !isDirective(node, context.getAncestors())) { | |
134 | context.report({ node, messageId: "unusedExpression" }); | |
135 | } | |
136 | } | |
137 | }; | |
138 | ||
139 | } | |
140 | }; |