123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- /**
- * @fileoverview Rule to flag assignment in a conditional statement's test expression
- * @author Stephen Murray <spmurrayzzz>
- */
- "use strict";
- //------------------------------------------------------------------------------
- // Requirements
- //------------------------------------------------------------------------------
- const astUtils = require("./utils/ast-utils");
- //------------------------------------------------------------------------------
- // Helpers
- //------------------------------------------------------------------------------
- const TEST_CONDITION_PARENT_TYPES = new Set(["IfStatement", "WhileStatement", "DoWhileStatement", "ForStatement", "ConditionalExpression"]);
- const NODE_DESCRIPTIONS = {
- DoWhileStatement: "a 'do...while' statement",
- ForStatement: "a 'for' statement",
- IfStatement: "an 'if' statement",
- WhileStatement: "a 'while' statement"
- };
- //------------------------------------------------------------------------------
- // Rule Definition
- //------------------------------------------------------------------------------
- /** @type {import('../shared/types').Rule} */
- module.exports = {
- meta: {
- type: "problem",
- docs: {
- description: "Disallow assignment operators in conditional expressions",
- recommended: true,
- url: "https://eslint.org/docs/rules/no-cond-assign"
- },
- schema: [
- {
- enum: ["except-parens", "always"]
- }
- ],
- messages: {
- unexpected: "Unexpected assignment within {{type}}.",
- // must match JSHint's error message
- missing: "Expected a conditional expression and instead saw an assignment."
- }
- },
- create(context) {
- const prohibitAssign = (context.options[0] || "except-parens");
- const sourceCode = context.getSourceCode();
- /**
- * Check whether an AST node is the test expression for a conditional statement.
- * @param {!Object} node The node to test.
- * @returns {boolean} `true` if the node is the text expression for a conditional statement; otherwise, `false`.
- */
- function isConditionalTestExpression(node) {
- return node.parent &&
- TEST_CONDITION_PARENT_TYPES.has(node.parent.type) &&
- node === node.parent.test;
- }
- /**
- * Given an AST node, perform a bottom-up search for the first ancestor that represents a conditional statement.
- * @param {!Object} node The node to use at the start of the search.
- * @returns {?Object} The closest ancestor node that represents a conditional statement.
- */
- function findConditionalAncestor(node) {
- let currentAncestor = node;
- do {
- if (isConditionalTestExpression(currentAncestor)) {
- return currentAncestor.parent;
- }
- } while ((currentAncestor = currentAncestor.parent) && !astUtils.isFunction(currentAncestor));
- return null;
- }
- /**
- * Check whether the code represented by an AST node is enclosed in two sets of parentheses.
- * @param {!Object} node The node to test.
- * @returns {boolean} `true` if the code is enclosed in two sets of parentheses; otherwise, `false`.
- */
- function isParenthesisedTwice(node) {
- const previousToken = sourceCode.getTokenBefore(node, 1),
- nextToken = sourceCode.getTokenAfter(node, 1);
- return astUtils.isParenthesised(sourceCode, node) &&
- previousToken && astUtils.isOpeningParenToken(previousToken) && previousToken.range[1] <= node.range[0] &&
- astUtils.isClosingParenToken(nextToken) && nextToken.range[0] >= node.range[1];
- }
- /**
- * Check a conditional statement's test expression for top-level assignments that are not enclosed in parentheses.
- * @param {!Object} node The node for the conditional statement.
- * @returns {void}
- */
- function testForAssign(node) {
- if (node.test &&
- (node.test.type === "AssignmentExpression") &&
- (node.type === "ForStatement"
- ? !astUtils.isParenthesised(sourceCode, node.test)
- : !isParenthesisedTwice(node.test)
- )
- ) {
- context.report({
- node: node.test,
- messageId: "missing"
- });
- }
- }
- /**
- * Check whether an assignment expression is descended from a conditional statement's test expression.
- * @param {!Object} node The node for the assignment expression.
- * @returns {void}
- */
- function testForConditionalAncestor(node) {
- const ancestor = findConditionalAncestor(node);
- if (ancestor) {
- context.report({
- node,
- messageId: "unexpected",
- data: {
- type: NODE_DESCRIPTIONS[ancestor.type] || ancestor.type
- }
- });
- }
- }
- if (prohibitAssign === "always") {
- return {
- AssignmentExpression: testForConditionalAncestor
- };
- }
- return {
- DoWhileStatement: testForAssign,
- ForStatement: testForAssign,
- IfStatement: testForAssign,
- WhileStatement: testForAssign,
- ConditionalExpression: testForAssign
- };
- }
- };
|