@rushstack/eslint-plugin
Version:
An ESLint plugin providing supplementary rules for use with the @rushstack/eslint-config package
90 lines • 4.8 kB
JavaScript
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { TSESTree } from '@typescript-eslint/utils';
export const MESSAGE_ID = 'error-pair-react-dom-render-unmount';
const pairReactDomRenderUnmountRule = {
defaultOptions: [],
meta: {
type: 'problem',
messages: {
[MESSAGE_ID]: 'Pair the render and unmount calls to avoid memory leaks.'
},
schema: [],
docs: {
description: 'Pair ReactDOM "render" and "unmount" calls in one file.' +
' If a ReactDOM render tree is not unmounted when disposed, it will cause a memory leak.',
url: 'https://www.npmjs.com/package/@rushstack/eslint-plugin'
}
},
create: (context) => {
const renderCallExpressions = [];
const unmountCallExpressions = [];
let reactDomImportNamespaceName;
let reactDomRenderFunctionName;
let reactDomUnmountFunctionName;
const isFunctionCallExpression = (node, methodName) => {
return node.callee.type === TSESTree.AST_NODE_TYPES.Identifier && node.callee.name === methodName;
};
const isNamespaceCallExpression = (node, namespaceName, methodName) => {
if (node.callee.type === TSESTree.AST_NODE_TYPES.MemberExpression) {
const { object, property } = node.callee;
if (object.type === TSESTree.AST_NODE_TYPES.Identifier && object.name === namespaceName) {
return ((property.type === TSESTree.AST_NODE_TYPES.Identifier && property.name === methodName) ||
(property.type === TSESTree.AST_NODE_TYPES.Literal && property.value === methodName));
}
}
return false;
};
return {
ImportDeclaration: (node) => {
// Extract the name for the 'react-dom' namespace import
if (node.source.value === 'react-dom') {
if (!reactDomImportNamespaceName) {
const namespaceSpecifier = node.specifiers.find((s) => s.type === TSESTree.AST_NODE_TYPES.ImportNamespaceSpecifier);
if (namespaceSpecifier) {
reactDomImportNamespaceName = namespaceSpecifier.local.name;
}
else {
const defaultSpecifier = node.specifiers.find((s) => s.type === TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier);
if (defaultSpecifier) {
reactDomImportNamespaceName = defaultSpecifier.local.name;
}
}
}
if (!reactDomRenderFunctionName || !reactDomUnmountFunctionName) {
const importSpecifiers = node.specifiers.filter((s) => s.type === TSESTree.AST_NODE_TYPES.ImportSpecifier);
for (const importSpecifier of importSpecifiers) {
const name = 'name' in importSpecifier.imported ? importSpecifier.imported.name : undefined;
if (name === 'render') {
reactDomRenderFunctionName = importSpecifier.local.name;
}
else if (name === 'unmountComponentAtNode') {
reactDomUnmountFunctionName = importSpecifier.local.name;
}
}
}
}
},
CallExpression: (node) => {
if (isNamespaceCallExpression(node, reactDomImportNamespaceName, 'render') ||
isFunctionCallExpression(node, reactDomRenderFunctionName)) {
renderCallExpressions.push(node);
}
else if (isNamespaceCallExpression(node, reactDomImportNamespaceName, 'unmountComponentAtNode') ||
isFunctionCallExpression(node, reactDomUnmountFunctionName)) {
unmountCallExpressions.push(node);
}
},
// eslint-disable-next-line @typescript-eslint/naming-convention
'Program:exit': (node) => {
if (renderCallExpressions.length !== unmountCallExpressions.length) {
renderCallExpressions.concat(unmountCallExpressions).forEach((callExpression) => {
context.report({ node: callExpression, messageId: MESSAGE_ID });
});
}
}
};
}
};
export { pairReactDomRenderUnmountRule };
//# sourceMappingURL=pair-react-dom-render-unmount.js.map