eslint-plugin-unicorn
Version:
More than 100 powerful ESLint rules
171 lines (142 loc) • 4.54 kB
JavaScript
import {isNodeValueNotDomNode} from './utils/index.js';
import {isMethodCall, isStringLiteral, isNullLiteral} from './ast/index.js';
const MESSAGE_ID = 'prefer-query-selector';
const messages = {
[MESSAGE_ID]: 'Prefer `.{{replacement}}()` over `.{{method}}()`.',
};
const disallowedIdentifierNames = new Map([
['getElementById', 'querySelector'],
['getElementsByClassName', 'querySelectorAll'],
['getElementsByTagName', 'querySelectorAll'],
['getElementsByName', 'querySelectorAll'],
]);
const getReplacementForId = value => `#${value}`;
const getReplacementForClass = value => value.match(/\S+/g).map(className => `.${className}`).join('');
const getReplacementForName = (value, originQuote) => `[name=${wrapQuoted(value, originQuote)}]`;
const getQuotedReplacement = (node, value) => {
const leftQuote = node.raw.charAt(0);
const rightQuote = node.raw.at(-1);
return `${leftQuote}${value}${rightQuote}`;
};
const wrapQuoted = (value, originalQuote) => {
switch (originalQuote) {
case '\'': {
return `"${value}"`;
}
case '"': {
return `'${value}'`;
}
case '`': {
return `'${value}'`;
}
// No default
}
};
function * getLiteralFix(fixer, node, identifierName) {
let replacement = node.raw;
if (identifierName === 'getElementById') {
replacement = getQuotedReplacement(node, getReplacementForId(node.value));
}
if (identifierName === 'getElementsByClassName') {
replacement = getQuotedReplacement(node, getReplacementForClass(node.value));
}
if (identifierName === 'getElementsByName') {
const quoted = node.raw.charAt(0);
replacement = getQuotedReplacement(node, getReplacementForName(node.value, quoted));
}
yield fixer.replaceText(node, replacement);
}
function * getTemplateLiteralFix(fixer, node, identifierName) {
yield fixer.insertTextAfter(node, '`');
yield fixer.insertTextBefore(node, '`');
for (const templateElement of node.quasis) {
if (identifierName === 'getElementById') {
yield fixer.replaceText(
templateElement,
getReplacementForId(templateElement.value.cooked),
);
}
if (identifierName === 'getElementsByClassName') {
yield fixer.replaceText(
templateElement,
getReplacementForClass(templateElement.value.cooked),
);
}
if (identifierName === 'getElementsByName') {
const quoted = node.raw ? node.raw.charAt(0) : '"';
yield fixer.replaceText(
templateElement,
getReplacementForName(templateElement.value.cooked, quoted),
);
}
}
}
const canBeFixed = node =>
isNullLiteral(node)
|| (isStringLiteral(node) && Boolean(node.value.trim()))
|| (
node.type === 'TemplateLiteral'
&& node.expressions.length === 0
&& node.quasis.some(templateElement => templateElement.value.cooked.trim())
);
const hasValue = node => {
if (node.type === 'Literal') {
return node.value;
}
return true;
};
const fix = (node, identifierName, preferredSelector) => {
const nodeToBeFixed = node.arguments[0];
if (identifierName === 'getElementsByTagName' || !hasValue(nodeToBeFixed)) {
return fixer => fixer.replaceText(node.callee.property, preferredSelector);
}
const getArgumentFix = nodeToBeFixed.type === 'Literal' ? getLiteralFix : getTemplateLiteralFix;
return function * (fixer) {
yield * getArgumentFix(fixer, nodeToBeFixed, identifierName);
yield fixer.replaceText(node.callee.property, preferredSelector);
};
};
/** @param {import('eslint').Rule.RuleContext} context */
const create = () => ({
CallExpression(node) {
if (
!isMethodCall(node, {
methods: ['getElementById', 'getElementsByClassName', 'getElementsByTagName', 'getElementsByName'],
argumentsLength: 1,
optionalCall: false,
optionalMember: false,
})
|| isNodeValueNotDomNode(node.callee.object)
) {
return;
}
const method = node.callee.property.name;
const preferredSelector = disallowedIdentifierNames.get(method);
const problem = {
node: node.callee.property,
messageId: MESSAGE_ID,
data: {
replacement: preferredSelector,
method,
},
};
if (canBeFixed(node.arguments[0])) {
problem.fix = fix(node, method, preferredSelector);
}
return problem;
},
});
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()` and `.getElementsByName()`.',
recommended: true,
},
fixable: 'code',
messages,
},
};
export default config;