UNPKG

eslint-plugin-unicorn

Version:
195 lines (164 loc) 4.3 kB
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;