132 lines
3.6 KiB
JavaScript
132 lines
3.6 KiB
JavaScript
|
'use strict';
|
||
|
const {methodCallSelector, notDomNodeSelector} = require('./selectors/index.js');
|
||
|
const {isStringLiteral, isNullLiteral} = require('./ast/index.js');
|
||
|
|
||
|
const MESSAGE_ID = 'prefer-query-selector';
|
||
|
const messages = {
|
||
|
[MESSAGE_ID]: 'Prefer `.{{replacement}}()` over `.{{method}}()`.',
|
||
|
};
|
||
|
|
||
|
const selector = [
|
||
|
methodCallSelector({
|
||
|
methods: ['getElementById', 'getElementsByClassName', 'getElementsByTagName'],
|
||
|
argumentsLength: 1,
|
||
|
}),
|
||
|
notDomNodeSelector('callee.object'),
|
||
|
].join('');
|
||
|
|
||
|
const disallowedIdentifierNames = new Map([
|
||
|
['getElementById', 'querySelector'],
|
||
|
['getElementsByClassName', 'querySelectorAll'],
|
||
|
['getElementsByTagName', 'querySelectorAll'],
|
||
|
]);
|
||
|
|
||
|
const getReplacementForId = value => `#${value}`;
|
||
|
const getReplacementForClass = value => value.match(/\S+/g).map(className => `.${className}`).join('');
|
||
|
|
||
|
const getQuotedReplacement = (node, value) => {
|
||
|
const leftQuote = node.raw.charAt(0);
|
||
|
const rightQuote = node.raw.charAt(node.raw.length - 1);
|
||
|
return `${leftQuote}${value}${rightQuote}`;
|
||
|
};
|
||
|
|
||
|
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));
|
||
|
}
|
||
|
|
||
|
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),
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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 = () => ({
|
||
|
[selector](node) {
|
||
|
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} */
|
||
|
module.exports = {
|
||
|
create,
|
||
|
meta: {
|
||
|
type: 'suggestion',
|
||
|
docs: {
|
||
|
description: 'Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`.',
|
||
|
},
|
||
|
fixable: 'code',
|
||
|
messages,
|
||
|
},
|
||
|
};
|