eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
156 lines (137 loc) • 4.11 kB
JavaScript
import {
getCallArgumentText,
getOutermostChainAndTypeScriptExpression,
hasOptionalChainElement,
isBoolean,
isBooleanContext,
isMap,
isSet,
isString,
isWeakMap,
isWeakSet,
unwrapChainAndTypeScriptExpression,
wouldRemoveComments,
} from './utils/index.js';
// `map[key]` reads a property of the `Map` object, not an entry, and `string[index]` is a character read, so for either one `Object.hasOwn()` is not a replacement for the computed access.
const isKnownNonObjectReceiver = (node, context) =>
[isMap, isSet, isWeakMap, isWeakSet, isString].some(isType => isType(node, context));
const MESSAGE_ID = 'no-computed-property-existence-check/error';
const MESSAGE_ID_SUGGESTION = 'no-computed-property-existence-check/suggestion';
const messages = {
[MESSAGE_ID]: 'Do not use a dynamic object property existence check.',
[MESSAGE_ID_SUGGESTION]: 'Use `Object.hasOwn()`.',
};
function isStaticPropertyKey(node) {
node = unwrapChainAndTypeScriptExpression(node);
return node.type === 'Literal'
|| node.type === 'PrivateIdentifier'
|| (
node.type === 'TemplateLiteral'
&& node.expressions.length === 0
)
|| (
node.type === 'UnaryExpression'
&& (node.operator === '-' || node.operator === '+')
&& node.argument.type === 'Literal'
&& (typeof node.argument.value === 'number' || typeof node.argument.value === 'bigint')
);
}
function isUsedAsExistenceCheck(node, context) {
return isBooleanContext(getOutermostChainAndTypeScriptExpression(node), context);
}
function isSimpleInSuggestionOperand(node) {
return node.type === 'Identifier' || node.type === 'ThisExpression';
}
function getObjectHasOwnText(object, property, context) {
return `Object.hasOwn(${getCallArgumentText(object, context)}, ${getCallArgumentText(property, context)})`;
}
function hasLeadingComments(node, context) {
return context.sourceCode.getCommentsBefore(node).length > 0;
}
function getSuggestion(node, object, property, context) {
if (
object.type === 'Super'
|| object.type === 'ChainExpression'
|| property.type === 'ChainExpression'
|| getOutermostChainAndTypeScriptExpression(node) !== node
|| hasOptionalChainElement(node)
|| hasOptionalChainElement(object)
|| hasOptionalChainElement(property)
|| (
node.type === 'BinaryExpression'
&& (!isSimpleInSuggestionOperand(object) || !isSimpleInSuggestionOperand(property))
)
|| hasLeadingComments(object, context)
|| hasLeadingComments(property, context)
|| wouldRemoveComments(context, node, [object, property])
) {
return;
}
return [
{
messageId: MESSAGE_ID_SUGGESTION,
fix: fixer => fixer.replaceText(node, getObjectHasOwnText(object, property, context)),
},
];
}
/**
@param {import('eslint').Rule.RuleContext} context
*/
const create = context => {
context.on('MemberExpression', node => {
if (!(
node.computed
&& !isStaticPropertyKey(node.property)
&& isUsedAsExistenceCheck(node, context)
)) {
return;
}
// A property whose value type is a known boolean is a value read, not an existence check (and `no-unnecessary-boolean-comparison` may rewrite a comparison into this form).
if (isBoolean(node, context)) {
return;
}
if (isKnownNonObjectReceiver(node.object, context)) {
return;
}
return {
node,
messageId: MESSAGE_ID,
suggest: getSuggestion(node, node.object, node.property, context),
};
});
context.on('BinaryExpression', node => {
if (
node.operator !== 'in'
|| isStaticPropertyKey(node.left)
// `key in map` is not a `Map#has()`, and `key in string` throws, so neither is a property lookup
|| isKnownNonObjectReceiver(node.right, context)
) {
return;
}
return {
node,
messageId: MESSAGE_ID,
suggest: getSuggestion(node, node.right, node.left, context),
};
});
};
/**
@type {import('eslint').Rule.RuleModule}
*/
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Disallow dynamic object property existence checks.',
recommended: true,
},
hasSuggestions: true,
schema: [],
messages,
languages: [
'js/js',
],
},
};
export default config;