124 lines
3.4 KiB
JavaScript
124 lines
3.4 KiB
JavaScript
import {isParenthesized, hasSideEffect} from '@eslint-community/eslint-utils';
|
|
import {isMethodCall} from './ast/index.js';
|
|
import {
|
|
getParenthesizedText,
|
|
isNodeValueNotDomNode,
|
|
isValueNotUsable,
|
|
needsSemicolon,
|
|
shouldAddParenthesesToMemberExpressionObject,
|
|
} from './utils/index.js';
|
|
|
|
const ERROR_MESSAGE_ID = 'error';
|
|
const SUGGESTION_MESSAGE_ID = 'suggestion';
|
|
const messages = {
|
|
[ERROR_MESSAGE_ID]: 'Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.',
|
|
[SUGGESTION_MESSAGE_ID]: 'Replace `parentNode.removeChild(childNode)` with `childNode{{dotOrQuestionDot}}remove()`.',
|
|
};
|
|
|
|
// TODO: Don't check node.type twice
|
|
const isMemberExpressionOptionalObject = node =>
|
|
node.parent.type === 'MemberExpression'
|
|
&& node.parent.object === node
|
|
&& (
|
|
node.parent.optional
|
|
|| (node.type === 'MemberExpression' && isMemberExpressionOptionalObject(node.object))
|
|
);
|
|
|
|
/** @param {import('eslint').Rule.RuleContext} context */
|
|
const create = context => {
|
|
const {sourceCode} = context;
|
|
|
|
return {
|
|
CallExpression(node) {
|
|
if (
|
|
!isMethodCall(node, {
|
|
method: 'removeChild',
|
|
argumentsLength: 1,
|
|
optionalCall: false,
|
|
})
|
|
|| isNodeValueNotDomNode(node.callee.object)
|
|
|| isNodeValueNotDomNode(node.arguments[0])
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const parentNode = node.callee.object;
|
|
const childNode = node.arguments[0];
|
|
|
|
const problem = {
|
|
node,
|
|
messageId: ERROR_MESSAGE_ID,
|
|
};
|
|
|
|
const isOptionalParentNode = isMemberExpressionOptionalObject(parentNode);
|
|
|
|
const createFix = (optional = false) => fixer => {
|
|
let childNodeText = getParenthesizedText(childNode, context);
|
|
if (
|
|
!isParenthesized(childNode, sourceCode)
|
|
&& shouldAddParenthesesToMemberExpressionObject(childNode, context)
|
|
) {
|
|
childNodeText = `(${childNodeText})`;
|
|
}
|
|
|
|
if (needsSemicolon(sourceCode.getTokenBefore(node), context, childNodeText)) {
|
|
childNodeText = `;${childNodeText}`;
|
|
}
|
|
|
|
return fixer.replaceText(node, `${childNodeText}${optional ? '?' : ''}.remove()`);
|
|
};
|
|
|
|
if (!hasSideEffect(parentNode, sourceCode) && isValueNotUsable(node)) {
|
|
if (!isOptionalParentNode) {
|
|
problem.fix = createFix(false);
|
|
return problem;
|
|
}
|
|
|
|
// The most common case `foo?.parentNode.remove(foo)`
|
|
// TODO: Allow case like `foo.bar?.parentNode.remove(foo.bar)`
|
|
if (
|
|
node.callee.type === 'MemberExpression'
|
|
&& !node.callee.optional
|
|
&& parentNode.type === 'MemberExpression'
|
|
&& parentNode.optional
|
|
&& !parentNode.computed
|
|
&& parentNode.property.type === 'Identifier'
|
|
&& parentNode.property.name === 'parentNode'
|
|
&& parentNode.object.type === 'Identifier'
|
|
&& childNode.type === 'Identifier'
|
|
&& parentNode.object.name === childNode.name
|
|
) {
|
|
problem.fix = createFix(true);
|
|
return problem;
|
|
}
|
|
}
|
|
|
|
problem.suggest = (
|
|
isOptionalParentNode ? [true, false] : [false]
|
|
).map(optional => ({
|
|
messageId: SUGGESTION_MESSAGE_ID,
|
|
data: {dotOrQuestionDot: optional ? '?.' : '.'},
|
|
fix: createFix(optional),
|
|
}));
|
|
|
|
return problem;
|
|
},
|
|
};
|
|
};
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
const config = {
|
|
create,
|
|
meta: {
|
|
type: 'suggestion',
|
|
docs: {
|
|
description: 'Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.',
|
|
recommended: 'unopinionated',
|
|
},
|
|
fixable: 'code',
|
|
hasSuggestions: true,
|
|
messages,
|
|
},
|
|
};
|
|
|
|
export default config;
|