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