UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

211 lines (200 loc) 8.51 kB
import { isNodeOfType } from 'eslint-codemod-utils'; import { JSXAttribute } from '../../ast-nodes/jsx-attribute'; import { JSXElementHelper } from '../../ast-nodes/jsx-element-helper'; import { createLintRule } from '../utils/create-lint-rule'; const PROP_NAME = 'hasCloseButton'; const MODAL_DIALOG_PACKAGE = '@atlaskit/modal-dialog'; const MODAL_DIALOG_IMPORT_SOURCE = '@atlaskit/modal-dialog/modal-dialog'; const MODAL_HEADER_IMPORT_SOURCE = '@atlaskit/modal-dialog/modal-header'; const CLOSE_BUTTON_IMPORT_SOURCE = '@atlaskit/modal-dialog/close-button'; // Lint rule message const message = '`hasCloseButton` should be set to `true` or the `CloseButton` component should be used to make modal dialog accessible.'; // Fix messages export const addHasCloseButtonProp = 'Add `hasCloseButton` prop.'; export const setHasCloseButtonPropToTrue = 'Set `hasCloseButton` prop to `true`.'; export const useCloseButtonOrNewProp = 'Set `hasCloseButton` prop to `true` in `ModalHeader` or use `CloseButton` export if customization is desired.'; export const ruleName = __dirname.split('/').slice(-1)[0]; const rule = createLintRule({ meta: { name: ruleName, type: 'suggestion', fixable: 'code', hasSuggestions: true, docs: { description: "Encourages makers to use close button in Atlassian Design System's modal dialog component.", recommended: true, severity: 'warn' }, messages: { modalHeaderMissingHasCloseButtonProp: message, modalHeaderHasCloseButtonPropIsFalse: message, noCloseButtonExists: message } }, create(context) { // List of component's locally imported names that match let defaultImportLocalName; let modalHeaderLocalName; let closeButtonLocalName; return { // Only run rule in files where the package is imported ImportDeclaration(node) { node.specifiers.forEach(identifier => { if ((node.source.value === MODAL_DIALOG_PACKAGE || node.source.value === MODAL_DIALOG_IMPORT_SOURCE) && isNodeOfType(identifier, 'ImportDefaultSpecifier')) { defaultImportLocalName = identifier.local.name; } else if (node.source.value === MODAL_HEADER_IMPORT_SOURCE && isNodeOfType(identifier, 'ImportDefaultSpecifier')) { modalHeaderLocalName = identifier.local.name; } else if ((node.source.value === MODAL_DIALOG_PACKAGE || node.source.value === CLOSE_BUTTON_IMPORT_SOURCE) && isNodeOfType(identifier, 'ImportSpecifier') && 'name' in identifier.imported) { const importName = identifier.imported.name; const localName = identifier.local.name; if (importName === 'ModalHeader') { modalHeaderLocalName = localName; } else if (importName === 'CloseButton') { closeButtonLocalName = localName; } } else if (node.source.value === CLOSE_BUTTON_IMPORT_SOURCE && isNodeOfType(identifier, 'ImportDefaultSpecifier')) { closeButtonLocalName = identifier.local.name; } }); }, JSXElement(node) { if (!isNodeOfType(node, 'JSXElement')) { return; } if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) { return; } const { name } = node.openingElement.name; if (![defaultImportLocalName, modalHeaderLocalName].includes(name)) { return; } let modalHeaderNode = null; let closeButtonNode = null; const checkNode = node => { if (modalHeaderNode && closeButtonNode) { return; } // Add expression container's body if an expression container if (isNodeOfType(node, 'JSXExpressionContainer')) { const expression = node.expression; if (isNodeOfType(expression, 'ArrowFunctionExpression') || isNodeOfType(expression, 'FunctionExpression')) { if (isNodeOfType(expression.body, 'JSXElement') || isNodeOfType(expression.body, 'JSXFragment')) { searchExpression(expression.body); } else if (isNodeOfType(expression.body, 'BlockStatement')) { expression.body.body.forEach(statement => { if (isNodeOfType(statement, 'ReturnStatement') && statement.argument) { searchExpression(statement.argument); } }); } } else if (isNodeOfType(expression, 'LogicalExpression')) { const { left, right } = expression; [left, right].forEach(e => { searchExpression(e); }); } else { searchExpression(expression); } return; } // Skip if not a JSX Element if (!isNodeOfType(node, 'JSXElement')) { return; } // Skip if opening element is not an identifier if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) { return; } // if child is CloseButton, return true if (node.openingElement.name.name === closeButtonLocalName) { closeButtonNode = node; } else if (node.openingElement.name.name === modalHeaderLocalName) { modalHeaderNode = node; } if (node.children) { searchNode(node); } }; const searchExpression = expression => { if (isNodeOfType(expression, 'JSXElement')) { searchNode(expression, true); } else if (isNodeOfType(expression, 'JSXFragment')) { searchJSXFragment(expression); } }; const searchNode = (node, searchSelf = false) => { if (searchSelf) { checkNode(node); } for (let child of node.children) { checkNode(child); } }; const searchJSXFragment = node => { if (isNodeOfType(node, 'JSXFragment')) { for (let child of node.children) { checkNode(child); } } }; if (name === defaultImportLocalName) { searchNode(node); // If there is a close button, skip the rest, as this satisfies the // rule. If there is a modal header, it will be recognized in later // scans, so don't add duplicate errors if (closeButtonNode || modalHeaderNode) { return; // No close button or modal header exists } else { return context.report({ node, messageId: 'noCloseButtonExists' }); } } else if (name === modalHeaderLocalName) { modalHeaderNode = node; searchNode(node); // If there is a close button, skip the rest, as this satisfies the rule. if (closeButtonNode) { return; } // No close button exists, so check the modal header const prop = JSXElementHelper.getAttributeByName(modalHeaderNode, PROP_NAME); // If the prop exists if (prop) { const attrValue = JSXAttribute.getValue(prop); // If the value is a boolean with value `false` if ((attrValue === null || attrValue === void 0 ? void 0 : attrValue.type) === 'ExpressionStatement Literal' && (attrValue === null || attrValue === void 0 ? void 0 : attrValue.value) === false) { return context.report({ node: modalHeaderNode, messageId: 'modalHeaderHasCloseButtonPropIsFalse', suggest: [{ desc: setHasCloseButtonPropToTrue, // Set to true by setting to boolean HTML/JSX attribute fix: fixer => [fixer.replaceText(prop, PROP_NAME)] }] }); } // If the prop does not exist } else { return context.report({ node: modalHeaderNode, messageId: 'modalHeaderMissingHasCloseButtonProp', suggest: [{ desc: addHasCloseButtonProp, fix: fixer => [fixer.insertTextAfter(modalHeaderNode.openingElement.name, ` ${PROP_NAME}`)] }] }); } } } }; } }); // eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports export default rule;