176 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			176 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
/**
 | 
						|
 * @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('../types').Rule.RuleModule} */
 | 
						|
module.exports = {
 | 
						|
	meta: {
 | 
						|
		type: "problem",
 | 
						|
 | 
						|
		defaultOptions: ["except-parens"],
 | 
						|
 | 
						|
		docs: {
 | 
						|
			description:
 | 
						|
				"Disallow assignment operators in conditional expressions",
 | 
						|
			recommended: true,
 | 
						|
			url: "https://eslint.org/docs/latest/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;
 | 
						|
		const sourceCode = context.sourceCode;
 | 
						|
 | 
						|
		/**
 | 
						|
		 * 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,
 | 
						|
		};
 | 
						|
	},
 | 
						|
};
 |