UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

124 lines (119 loc) 4.62 kB
// eslint-disable-next-line import/no-extraneous-dependencies import { isNodeOfType } from 'eslint-codemod-utils'; import { createLintRule } from '../utils/create-lint-rule'; import { addProp } from './add-prop'; import { setPropToTrue } from './set-prop-to-true'; export const RULE_NAME = 'use-should-render-to-parent'; const PROP_NAME = 'shouldRenderToParent'; const message = `Setting the \`${PROP_NAME}\` prop to anything other than \`true\` causes accessibility issues. Only set to \`false\` as a last resort.`; const supportedImports = [{ source: '@atlaskit/popup', allowsDefaultImport: true, namedImport: 'Popup' }, { source: '@atlaskit/popup/popup', namedImport: 'Popup' }, { source: '@atlaskit/dropdown-menu', allowsDefaultImport: true }, { source: '@atlaskit/dropdown-menu/dropdown-menu', allowsDefaultImport: true }, { source: '@atlassian/entry-points/dropdown-trigger', namedImport: 'DropdownTrigger' }, { source: '@atlassian/entry-points/popup-trigger', namedImport: 'PopupTrigger' }]; const rule = createLintRule({ meta: { name: RULE_NAME, type: 'suggestion', docs: { description: `Encourages makers to use the \`${PROP_NAME}\` where possible in Atlassian Design System \`Popup\` and \`DropdownMenu\` components.`, recommended: true, severity: 'warn' }, messages: { missingShouldRenderToParentProp: `The default value of \`${PROP_NAME}\` is \`false\`. ${message}`, falseShouldRenderToParentProp: message }, hasSuggestions: true }, create(context) { const componentLocalNames = new Set(); return { ImportDeclaration(node) { const source = node.source.value; if (typeof source !== 'string') { return; } const supportedImport = supportedImports.find(component => component.source === source); if (!supportedImport) { return; } if (!node.specifiers.length) { return; } const defaultImport = node.specifiers.filter(spec => spec.type === 'ImportDefaultSpecifier'); const namedImports = node.specifiers.filter(spec => spec.type === 'ImportSpecifier'); if (defaultImport.length && defaultImport[0].local && supportedImport.allowsDefaultImport) { componentLocalNames.add(defaultImport[0].local.name); } if (!namedImports.length) { return; } // Iterate through all of them, only one should ever be found namedImports.forEach(namedImport => { if (namedImport.type === 'ImportSpecifier' && 'name' in namedImport.imported && namedImport.imported.name === supportedImport.namedImport) { componentLocalNames.add(namedImport.local.name); } }); }, JSXElement(node) { if (!isNodeOfType(node, 'JSXElement')) { return; } if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) { return; } const name = node.openingElement.name.name; if (componentLocalNames.has(name)) { const prop = node.openingElement.attributes.find(attr => isNodeOfType(attr, 'JSXAttribute') && isNodeOfType(attr.name, 'JSXIdentifier') && attr.name.name === PROP_NAME); // If the prop does not exist, throw if (!prop) { return context.report({ node: node.openingElement.name, messageId: 'missingShouldRenderToParentProp', suggest: [{ desc: addProp, fix: fixer => [fixer.insertTextAfter(node.openingElement.name, ` ${PROP_NAME}`)] }] }); } // If the prop is a boolean attribute with no value (set to `true`), // it's valid if (!('value' in prop) || prop.value === null) { return; } // If the prop has a falsy literal value or a falsy value in an // expression container, throw if (isNodeOfType(prop.value, 'Literal') && !prop.value.value || isNodeOfType(prop.value, 'JSXExpressionContainer') && prop.value.expression.type === 'Literal' && !prop.value.expression.value) { return context.report({ node: prop, messageId: 'falseShouldRenderToParentProp', suggest: [{ desc: setPropToTrue, fix: fixer => [fixer.replaceText(prop, PROP_NAME)] }] }); } } } }; } }); export default rule; export { addProp } from './add-prop'; export { setPropToTrue } from './set-prop-to-true';