eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
195 lines (164 loc) • 4.3 kB
JavaScript
import {getStaticValue as getStaticValueFromEslintUtilities} from '@eslint-community/eslint-utils';
import {
hasOptionalChainElement,
hasSameObjectShapePropertyCheck,
isKnownNonCollectionLengthOrSize,
isLengthOrSizeMemberExpression,
getStaticValueForControlFlow,
unwrapTypeScriptExpression,
} from './utils/index.js';
const MESSAGE_ID = 'no-impossible-length-comparison';
const messages = {
[MESSAGE_ID]: 'This comparison is always {{result}} because `.{{property}}` is always a non-negative integer.',
};
const flipOperator = {
'<': '>',
'<=': '>=',
'>': '<',
'>=': '<=',
'===': '===',
'!==': '!==',
'==': '==',
'!=': '!=',
};
const isKnownStaticProperty = node => {
node = unwrapTypeScriptExpression(node);
if (node.type === 'UnaryExpression' && (node.operator === '+' || node.operator === '-')) {
node = unwrapTypeScriptExpression(node.argument);
}
return node.type === 'MemberExpression'
&& (!node.computed || node.property.type === 'Literal')
&& node.object.type === 'Identifier'
&& (node.object.name === 'Math' || node.object.name === 'Number');
};
const getStaticComparisonValue = (node, context) => {
const staticValue = getStaticValueForControlFlow(node, context);
if (staticValue !== undefined) {
return staticValue.value;
}
return isKnownStaticProperty(node)
? getStaticValueFromEslintUtilities(node, context.sourceCode.getScope(node))?.value
: undefined;
};
function getComparisonSubject(node, context) {
const left = unwrapTypeScriptExpression(node.left);
const right = unwrapTypeScriptExpression(node.right);
if (isLengthOrSizeMemberExpression(left)) {
return {
memberExpression: left,
operator: node.operator,
value: getStaticComparisonValue(right, context),
};
}
if (isLengthOrSizeMemberExpression(right)) {
return {
memberExpression: right,
operator: flipOperator[node.operator],
value: getStaticComparisonValue(left, context),
};
}
}
function getConstantResult({operator, value}) {
if (
typeof value !== 'number'
|| !Number.isFinite(value)
) {
return;
}
switch (operator) {
case '<': {
return value <= 0 ? false : undefined;
}
case '<=': {
return value < 0 ? false : undefined;
}
case '>': {
return value < 0 ? true : undefined;
}
case '>=': {
return value <= 0 ? true : undefined;
}
case '===':
case '==': {
return value < 0 ? false : undefined;
}
case '!==':
case '!=': {
return value < 0 ? true : undefined;
}
default:
}
}
function hasOptionalChain(node) {
node = unwrapTypeScriptExpression(node);
if (node.type === 'ChainExpression' || hasOptionalChainElement(node)) {
return true;
}
if (node.type === 'MemberExpression') {
return hasOptionalChain(node.object);
}
if (node.type === 'CallExpression') {
return hasOptionalChain(node.callee);
}
return false;
}
function isOptionalChainReceiver(memberExpression) {
return hasOptionalChain(memberExpression.object);
}
function isCustomClassReceiver(memberExpression) {
const receiver = unwrapTypeScriptExpression(memberExpression.object);
return receiver.type === 'ThisExpression' || receiver.type === 'Super';
}
/**
@param {import('eslint').Rule.RuleContext} context
*/
const create = context => {
context.on('BinaryExpression', node => {
if (!Object.hasOwn(flipOperator, node.operator)) {
return;
}
const comparison = getComparisonSubject(node, context);
if (!comparison) {
return;
}
const {memberExpression} = comparison;
if (
isCustomClassReceiver(memberExpression)
|| isOptionalChainReceiver(memberExpression)
|| isKnownNonCollectionLengthOrSize(memberExpression, context)
|| hasSameObjectShapePropertyCheck({node, lengthOrSizeNode: memberExpression})
) {
return;
}
const result = getConstantResult(comparison);
if (result === undefined) {
return;
}
return {
node,
messageId: MESSAGE_ID,
data: {
property: memberExpression.property.name,
result: String(result),
},
};
});
};
/**
@type {import('eslint').Rule.RuleModule}
*/
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Disallow impossible comparisons against `.length` or `.size`.',
recommended: 'unopinionated',
},
messages,
languages: [
'js/js',
],
},
};
export default config;