line_push/node_modules/eslint-plugin-unicorn/rules/prefer-modern-dom-apis.js
2022-07-17 13:16:16 +08:00

137 lines
3.7 KiB
JavaScript

'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const isValueNotUsable = require('./utils/is-value-not-usable');
const methodSelector = require('./utils/method-selector');
const messages = {
replaceChildOrInsertBefore:
'Prefer `{{oldChildNode}}.{{preferredMethod}}({{newChildNode}})` over `{{parentNode}}.{{method}}({{newChildNode}}, {{oldChildNode}})`.',
insertAdjacentTextOrInsertAdjacentElement:
'Prefer `{{reference}}.{{preferredMethod}}({{content}})` over `{{reference}}.{{method}}({{position}}, {{content}})`.'
};
const replaceChildOrInsertBeforeSelector = [
methodSelector({
names: ['replaceChild', 'insertBefore'],
length: 2
}),
// We only allow Identifier for now
'[arguments.0.type="Identifier"]',
'[arguments.0.name!="undefined"]',
'[arguments.1.type="Identifier"]',
'[arguments.1.name!="undefined"]',
// This check makes sure that only the first method of chained methods with same identifier name e.g: parentNode.insertBefore(alfa, beta).insertBefore(charlie, delta); gets reported
'[callee.object.type="Identifier"]'
].join('');
const forbiddenMethods = new Map([
['replaceChild', 'replaceWith'],
['insertBefore', 'before']
]);
const checkForReplaceChildOrInsertBefore = (context, node) => {
const method = node.callee.property.name;
const parentNode = node.callee.object.name;
const [newChildNode, oldChildNode] = node.arguments.map(({name}) => name);
const preferredMethod = forbiddenMethods.get(method);
const fix = isValueNotUsable(node) ?
fixer => fixer.replaceText(
node,
`${oldChildNode}.${preferredMethod}(${newChildNode})`
) :
undefined;
return context.report({
node,
messageId: 'replaceChildOrInsertBefore',
data: {
parentNode,
method,
preferredMethod,
newChildNode,
oldChildNode
},
fix
});
};
const insertAdjacentTextOrInsertAdjacentElementSelector = [
methodSelector({
names: ['insertAdjacentText', 'insertAdjacentElement'],
length: 2
}),
// Position argument should be `string`
'[arguments.0.type="Literal"]',
// TODO: remove this limits on second argument
':matches([arguments.1.type="Literal"], [arguments.1.type="Identifier"])',
// TODO: remove this limits on callee
'[callee.object.type="Identifier"]'
].join('');
const positionReplacers = new Map([
['beforebegin', 'before'],
['afterbegin', 'prepend'],
['beforeend', 'append'],
['afterend', 'after']
]);
const checkForInsertAdjacentTextOrInsertAdjacentElement = (context, node) => {
const method = node.callee.property.name;
const [positionNode, contentNode] = node.arguments;
const position = positionNode.value;
// Return early when specified position value of first argument is not a recognized value.
if (!positionReplacers.has(position)) {
return;
}
const preferredMethod = positionReplacers.get(position);
const content = context.getSource(contentNode);
const reference = context.getSource(node.callee.object);
const fix = method === 'insertAdjacentElement' && !isValueNotUsable(node) ?
undefined :
// TODO: make a better fix, don't touch reference
fixer => fixer.replaceText(
node,
`${reference}.${preferredMethod}(${content})`
);
return context.report({
node,
messageId: 'insertAdjacentTextOrInsertAdjacentElement',
data: {
reference,
method,
preferredMethod,
position: context.getSource(positionNode),
content
},
fix
});
};
const create = context => {
return {
[replaceChildOrInsertBeforeSelector](node) {
checkForReplaceChildOrInsertBefore(context, node);
},
[insertAdjacentTextOrInsertAdjacentElementSelector](node) {
checkForInsertAdjacentTextOrInsertAdjacentElement(context, node);
}
};
};
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code',
messages
}
};