UNPKG

5.33 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to flag assignment in a conditional statement's test expression
3 * @author Stephen Murray <spmurrayzzz>
4 */
5"use strict";
6
7const astUtils = require("../ast-utils");
8
9const NODE_DESCRIPTIONS = {
10 DoWhileStatement: "a 'do...while' statement",
11 ForStatement: "a 'for' statement",
12 IfStatement: "an 'if' statement",
13 WhileStatement: "a 'while' statement"
14};
15
16//------------------------------------------------------------------------------
17// Rule Definition
18//------------------------------------------------------------------------------
19
20module.exports = {
21 meta: {
22 docs: {
23 description: "disallow assignment operators in conditional expressions",
24 category: "Possible Errors",
25 recommended: true
26 },
27
28 schema: [
29 {
30 enum: ["except-parens", "always"]
31 }
32 ]
33 },
34
35 create(context) {
36
37 const prohibitAssign = (context.options[0] || "except-parens");
38
39 const sourceCode = context.getSourceCode();
40
41 /**
42 * Check whether an AST node is the test expression for a conditional statement.
43 * @param {!Object} node The node to test.
44 * @returns {boolean} `true` if the node is the text expression for a conditional statement; otherwise, `false`.
45 */
46 function isConditionalTestExpression(node) {
47 return node.parent &&
48 node.parent.test &&
49 node === node.parent.test;
50 }
51
52 /**
53 * Given an AST node, perform a bottom-up search for the first ancestor that represents a conditional statement.
54 * @param {!Object} node The node to use at the start of the search.
55 * @returns {?Object} The closest ancestor node that represents a conditional statement.
56 */
57 function findConditionalAncestor(node) {
58 let currentAncestor = node;
59
60 do {
61 if (isConditionalTestExpression(currentAncestor)) {
62 return currentAncestor.parent;
63 }
64 } while ((currentAncestor = currentAncestor.parent) && !astUtils.isFunction(currentAncestor));
65
66 return null;
67 }
68
69 /**
70 * Check whether the code represented by an AST node is enclosed in parentheses.
71 * @param {!Object} node The node to test.
72 * @returns {boolean} `true` if the code is enclosed in parentheses; otherwise, `false`.
73 */
74 function isParenthesised(node) {
75 const previousToken = sourceCode.getTokenBefore(node),
76 nextToken = sourceCode.getTokenAfter(node);
77
78 return previousToken.value === "(" && previousToken.range[1] <= node.range[0] &&
79 nextToken.value === ")" && nextToken.range[0] >= node.range[1];
80 }
81
82 /**
83 * Check whether the code represented by an AST node is enclosed in two sets of parentheses.
84 * @param {!Object} node The node to test.
85 * @returns {boolean} `true` if the code is enclosed in two sets of parentheses; otherwise, `false`.
86 */
87 function isParenthesisedTwice(node) {
88 const previousToken = sourceCode.getTokenBefore(node, 1),
89 nextToken = sourceCode.getTokenAfter(node, 1);
90
91 return isParenthesised(node) &&
92 previousToken.value === "(" && previousToken.range[1] <= node.range[0] &&
93 nextToken.value === ")" && nextToken.range[0] >= node.range[1];
94 }
95
96 /**
97 * Check a conditional statement's test expression for top-level assignments that are not enclosed in parentheses.
98 * @param {!Object} node The node for the conditional statement.
99 * @returns {void}
100 */
101 function testForAssign(node) {
102 if (node.test &&
103 (node.test.type === "AssignmentExpression") &&
104 (node.type === "ForStatement" ?
105 !isParenthesised(node.test) :
106 !isParenthesisedTwice(node.test)
107 )
108 ) {
109
110 // must match JSHint's error message
111 context.report({
112 node,
113 loc: node.test.loc.start,
114 message: "Expected a conditional expression and instead saw an assignment."
115 });
116 }
117 }
118
119 /**
120 * Check whether an assignment expression is descended from a conditional statement's test expression.
121 * @param {!Object} node The node for the assignment expression.
122 * @returns {void}
123 */
124 function testForConditionalAncestor(node) {
125 const ancestor = findConditionalAncestor(node);
126
127 if (ancestor) {
128 context.report(ancestor, "Unexpected assignment within {{type}}.", {
129 type: NODE_DESCRIPTIONS[ancestor.type] || ancestor.type
130 });
131 }
132 }
133
134 if (prohibitAssign === "always") {
135 return {
136 AssignmentExpression: testForConditionalAncestor
137 };
138 }
139
140 return {
141 DoWhileStatement: testForAssign,
142 ForStatement: testForAssign,
143 IfStatement: testForAssign,
144 WhileStatement: testForAssign
145 };
146
147 }
148};