Every new change
This commit is contained in:
327
node_modules/eslint/lib/rules/yoda.js
generated
vendored
Normal file
327
node_modules/eslint/lib/rules/yoda.js
generated
vendored
Normal file
@ -0,0 +1,327 @@
|
||||
/**
|
||||
* @fileoverview Rule to require or disallow yoda comparisons
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
const astUtils = require("./utils/ast-utils");
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Determines whether an operator is a comparison operator.
|
||||
* @param {string} operator The operator to check.
|
||||
* @returns {boolean} Whether or not it is a comparison operator.
|
||||
*/
|
||||
function isComparisonOperator(operator) {
|
||||
return (/^(==|===|!=|!==|<|>|<=|>=)$/u).test(operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether an operator is an equality operator.
|
||||
* @param {string} operator The operator to check.
|
||||
* @returns {boolean} Whether or not it is an equality operator.
|
||||
*/
|
||||
function isEqualityOperator(operator) {
|
||||
return (/^(==|===)$/u).test(operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether an operator is one used in a range test.
|
||||
* Allowed operators are `<` and `<=`.
|
||||
* @param {string} operator The operator to check.
|
||||
* @returns {boolean} Whether the operator is used in range tests.
|
||||
*/
|
||||
function isRangeTestOperator(operator) {
|
||||
return ["<", "<="].indexOf(operator) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a non-Literal node is a negative number that should be
|
||||
* treated as if it were a single Literal node.
|
||||
* @param {ASTNode} node Node to test.
|
||||
* @returns {boolean} True if the node is a negative number that looks like a
|
||||
* real literal and should be treated as such.
|
||||
*/
|
||||
function looksLikeLiteral(node) {
|
||||
return (node.type === "UnaryExpression" &&
|
||||
node.operator === "-" &&
|
||||
node.prefix &&
|
||||
node.argument.type === "Literal" &&
|
||||
typeof node.argument.value === "number");
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to derive a Literal node from nodes that are treated like literals.
|
||||
* @param {ASTNode} node Node to normalize.
|
||||
* @param {number} [defaultValue] The default value to be returned if the node
|
||||
* is not a Literal.
|
||||
* @returns {ASTNode} One of the following options.
|
||||
* 1. The original node if the node is already a Literal
|
||||
* 2. A normalized Literal node with the negative number as the value if the
|
||||
* node represents a negative number literal.
|
||||
* 3. The Literal node which has the `defaultValue` argument if it exists.
|
||||
* 4. Otherwise `null`.
|
||||
*/
|
||||
function getNormalizedLiteral(node, defaultValue) {
|
||||
if (node.type === "Literal") {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (looksLikeLiteral(node)) {
|
||||
return {
|
||||
type: "Literal",
|
||||
value: -node.argument.value,
|
||||
raw: `-${node.argument.value}`
|
||||
};
|
||||
}
|
||||
|
||||
if (defaultValue) {
|
||||
return {
|
||||
type: "Literal",
|
||||
value: defaultValue,
|
||||
raw: String(defaultValue)
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether two expressions reference the same value. For example:
|
||||
* a = a
|
||||
* a.b = a.b
|
||||
* a[0] = a[0]
|
||||
* a['b'] = a['b']
|
||||
* @param {ASTNode} a Left side of the comparison.
|
||||
* @param {ASTNode} b Right side of the comparison.
|
||||
* @returns {boolean} True if both sides match and reference the same value.
|
||||
*/
|
||||
function same(a, b) {
|
||||
if (a.type !== b.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (a.type) {
|
||||
case "Identifier":
|
||||
return a.name === b.name;
|
||||
|
||||
case "Literal":
|
||||
return a.value === b.value;
|
||||
|
||||
case "MemberExpression": {
|
||||
const nameA = astUtils.getStaticPropertyName(a);
|
||||
|
||||
// x.y = x["y"]
|
||||
if (nameA !== null) {
|
||||
return (
|
||||
same(a.object, b.object) &&
|
||||
nameA === astUtils.getStaticPropertyName(b)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* x[0] = x[0]
|
||||
* x[y] = x[y]
|
||||
* x.y = x.y
|
||||
*/
|
||||
return (
|
||||
a.computed === b.computed &&
|
||||
same(a.object, b.object) &&
|
||||
same(a.property, b.property)
|
||||
);
|
||||
}
|
||||
|
||||
case "ThisExpression":
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
|
||||
docs: {
|
||||
description: "require or disallow \"Yoda\" conditions",
|
||||
category: "Best Practices",
|
||||
recommended: false,
|
||||
url: "https://eslint.org/docs/rules/yoda"
|
||||
},
|
||||
|
||||
schema: [
|
||||
{
|
||||
enum: ["always", "never"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
exceptRange: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
onlyEquality: {
|
||||
type: "boolean",
|
||||
default: false
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
|
||||
fixable: "code",
|
||||
messages: {
|
||||
expected: "Expected literal to be on the {{expectedSide}} side of {{operator}}."
|
||||
}
|
||||
},
|
||||
|
||||
create(context) {
|
||||
|
||||
// Default to "never" (!always) if no option
|
||||
const always = (context.options[0] === "always");
|
||||
const exceptRange = (context.options[1] && context.options[1].exceptRange);
|
||||
const onlyEquality = (context.options[1] && context.options[1].onlyEquality);
|
||||
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
/**
|
||||
* Determines whether node represents a range test.
|
||||
* A range test is a "between" test like `(0 <= x && x < 1)` or an "outside"
|
||||
* test like `(x < 0 || 1 <= x)`. It must be wrapped in parentheses, and
|
||||
* both operators must be `<` or `<=`. Finally, the literal on the left side
|
||||
* must be less than or equal to the literal on the right side so that the
|
||||
* test makes any sense.
|
||||
* @param {ASTNode} node LogicalExpression node to test.
|
||||
* @returns {boolean} Whether node is a range test.
|
||||
*/
|
||||
function isRangeTest(node) {
|
||||
const left = node.left,
|
||||
right = node.right;
|
||||
|
||||
/**
|
||||
* Determines whether node is of the form `0 <= x && x < 1`.
|
||||
* @returns {boolean} Whether node is a "between" range test.
|
||||
*/
|
||||
function isBetweenTest() {
|
||||
let leftLiteral, rightLiteral;
|
||||
|
||||
return (node.operator === "&&" &&
|
||||
(leftLiteral = getNormalizedLiteral(left.left)) &&
|
||||
(rightLiteral = getNormalizedLiteral(right.right, Number.POSITIVE_INFINITY)) &&
|
||||
leftLiteral.value <= rightLiteral.value &&
|
||||
same(left.right, right.left));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether node is of the form `x < 0 || 1 <= x`.
|
||||
* @returns {boolean} Whether node is an "outside" range test.
|
||||
*/
|
||||
function isOutsideTest() {
|
||||
let leftLiteral, rightLiteral;
|
||||
|
||||
return (node.operator === "||" &&
|
||||
(leftLiteral = getNormalizedLiteral(left.right, Number.NEGATIVE_INFINITY)) &&
|
||||
(rightLiteral = getNormalizedLiteral(right.left)) &&
|
||||
leftLiteral.value <= rightLiteral.value &&
|
||||
same(left.left, right.right));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether node is wrapped in parentheses.
|
||||
* @returns {boolean} Whether node is preceded immediately by an open
|
||||
* paren token and followed immediately by a close
|
||||
* paren token.
|
||||
*/
|
||||
function isParenWrapped() {
|
||||
return astUtils.isParenthesised(sourceCode, node);
|
||||
}
|
||||
|
||||
return (node.type === "LogicalExpression" &&
|
||||
left.type === "BinaryExpression" &&
|
||||
right.type === "BinaryExpression" &&
|
||||
isRangeTestOperator(left.operator) &&
|
||||
isRangeTestOperator(right.operator) &&
|
||||
(isBetweenTest() || isOutsideTest()) &&
|
||||
isParenWrapped());
|
||||
}
|
||||
|
||||
const OPERATOR_FLIP_MAP = {
|
||||
"===": "===",
|
||||
"!==": "!==",
|
||||
"==": "==",
|
||||
"!=": "!=",
|
||||
"<": ">",
|
||||
">": "<",
|
||||
"<=": ">=",
|
||||
">=": "<="
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a string representation of a BinaryExpression node with its sides/operator flipped around.
|
||||
* @param {ASTNode} node The BinaryExpression node
|
||||
* @returns {string} A string representation of the node with the sides and operator flipped
|
||||
*/
|
||||
function getFlippedString(node) {
|
||||
const tokenBefore = sourceCode.getTokenBefore(node);
|
||||
const operatorToken = sourceCode.getFirstTokenBetween(node.left, node.right, token => token.value === node.operator);
|
||||
const textBeforeOperator = sourceCode.getText().slice(sourceCode.getTokenBefore(operatorToken).range[1], operatorToken.range[0]);
|
||||
const textAfterOperator = sourceCode.getText().slice(operatorToken.range[1], sourceCode.getTokenAfter(operatorToken).range[0]);
|
||||
const leftText = sourceCode.getText().slice(node.range[0], sourceCode.getTokenBefore(operatorToken).range[1]);
|
||||
const firstRightToken = sourceCode.getTokenAfter(operatorToken);
|
||||
const rightText = sourceCode.getText().slice(firstRightToken.range[0], node.range[1]);
|
||||
|
||||
let prefix = "";
|
||||
|
||||
if (tokenBefore && tokenBefore.range[1] === node.range[0] &&
|
||||
!astUtils.canTokensBeAdjacent(tokenBefore, firstRightToken)) {
|
||||
prefix = " ";
|
||||
}
|
||||
|
||||
return prefix + rightText + textBeforeOperator + OPERATOR_FLIP_MAP[operatorToken.value] + textAfterOperator + leftText;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
return {
|
||||
BinaryExpression(node) {
|
||||
const expectedLiteral = always ? node.left : node.right;
|
||||
const expectedNonLiteral = always ? node.right : node.left;
|
||||
|
||||
// If `expectedLiteral` is not a literal, and `expectedNonLiteral` is a literal, raise an error.
|
||||
if (
|
||||
(expectedNonLiteral.type === "Literal" || looksLikeLiteral(expectedNonLiteral)) &&
|
||||
!(expectedLiteral.type === "Literal" || looksLikeLiteral(expectedLiteral)) &&
|
||||
!(!isEqualityOperator(node.operator) && onlyEquality) &&
|
||||
isComparisonOperator(node.operator) &&
|
||||
!(exceptRange && isRangeTest(context.getAncestors().pop()))
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "expected",
|
||||
data: {
|
||||
operator: node.operator,
|
||||
expectedSide: always ? "left" : "right"
|
||||
},
|
||||
fix: fixer => fixer.replaceText(node, getFlippedString(node))
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user