eslint-plugin-unicorn-x
Version:
More than 100 powerful ESLint rules
191 lines (165 loc) • 4.08 kB
JavaScript
import {
getPropertyName,
ReferenceTracker,
} from '@eslint-community/eslint-utils';
import {fixSpaceAroundKeyword} from './fix/index.js';
import {isMemberExpression, isMethodCall} from './ast/index.js';
const messages = {
'known-method':
'Prefer using `{{constructorName}}.prototype.{{methodName}}`.',
'unknown-method': 'Prefer using method from `{{constructorName}}.prototype`.',
};
const OBJECT_PROTOTYPE_METHODS = [
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
'toString',
'valueOf',
];
function getConstructorAndMethodName(
methodNode,
{sourceCode, globalReferences},
) {
if (!methodNode) {
return;
}
const isGlobalReference = globalReferences.has(methodNode);
if (isGlobalReference) {
const path = globalReferences.get(methodNode);
return {
isGlobalReference: true,
constructorName: 'Object',
methodName: path.at(-1),
};
}
if (
!isMemberExpression(methodNode, {
properties: undefined,
optional: false,
computed: undefined,
})
) {
return;
}
const objectNode = methodNode.object;
if (
!(
(objectNode.type === 'ArrayExpression' &&
objectNode.elements.length === 0) ||
(objectNode.type === 'ObjectExpression' &&
objectNode.properties.length === 0)
)
) {
return;
}
const constructorName =
objectNode.type === 'ArrayExpression' ? 'Array' : 'Object';
const methodName = getPropertyName(
methodNode,
sourceCode.getScope(methodNode),
);
return {
constructorName,
methodName,
};
}
function getProblem(callExpression, {sourceCode, globalReferences}) {
let methodNode;
if (
// `Reflect.apply([].foo, …)`
// `Reflect.apply({}.foo, …)`
isMethodCall(callExpression, {
object: 'Reflect',
method: 'apply',
minimumArguments: 1,
optionalCall: false,
optionalMember: false,
})
) {
methodNode = callExpression.arguments[0];
} else if (
// `[].foo.{apply,bind,call}(…)`
// `({}).foo.{apply,bind,call}(…)`
isMethodCall(callExpression, {
methods: ['apply', 'bind', 'call'],
optionalCall: false,
optionalMember: false,
})
) {
methodNode = callExpression.callee.object;
}
const {isGlobalReference, constructorName, methodName} =
getConstructorAndMethodName(methodNode, {sourceCode, globalReferences}) ??
{};
if (!constructorName) {
return;
}
return {
node: methodNode,
messageId: methodName ? 'known-method' : 'unknown-method',
data: {constructorName, methodName},
*fix(fixer) {
if (isGlobalReference) {
yield fixer.replaceText(
methodNode,
`${constructorName}.prototype.${methodName}`,
);
return;
}
if (isMemberExpression(methodNode)) {
const objectNode = methodNode.object;
yield fixer.replaceText(objectNode, `${constructorName}.prototype`);
if (
objectNode.type === 'ArrayExpression' ||
objectNode.type === 'ObjectExpression'
) {
yield* fixSpaceAroundKeyword(fixer, callExpression, sourceCode);
}
}
},
};
}
/** @param {import('eslint').Rule.RuleContext} context */
function create(context) {
const {sourceCode} = context;
const callExpressions = [];
context.on('CallExpression', (callExpression) => {
callExpressions.push(callExpression);
});
context.on('Program:exit', function* (program) {
const globalReferences = new WeakMap();
const tracker = new ReferenceTracker(sourceCode.getScope(program));
for (const {node, path} of tracker.iterateGlobalReferences(
Object.fromEntries(
OBJECT_PROTOTYPE_METHODS.map((method) => [
method,
{[ReferenceTracker.READ]: true},
]),
),
)) {
globalReferences.set(node, path);
}
for (const callExpression of callExpressions) {
yield getProblem(callExpression, {
sourceCode,
globalReferences,
});
}
});
}
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description:
'Prefer borrowing methods from the prototype instead of the instance.',
recommended: true,
},
fixable: 'code',
messages,
},
};
export default config;