UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

129 lines (124 loc) 4.28 kB
import { isNodeOfType } from 'eslint-codemod-utils'; import { createLintRule } from '../utils/create-lint-rule'; import { findProp } from '../utils/find-prop'; /** * Entry points whose default export is an icon-only control, where the `label` * prop is the sole source of the accessible name. */ const DEFAULT_IMPORT_SOURCES = new Set(['@atlaskit/button/icon/button', '@atlaskit/button/icon/link']); /** * Deprecated entry point that still re-exports the icon-only controls by name. */ const NAMED_IMPORT_SOURCES = new Set(['@atlaskit/button/new']); const NAMED_IMPORTS = new Set(['IconButton', 'LinkIconButton']); const normalize = value => value.trim(); /** * Resolves a `label` prop to a static string, or `undefined` when the value * cannot be determined statically. Dynamic values are intentionally skipped so * the rule never guesses at the contents of a variable, function call or * interpolated template. */ const getStaticLabel = attribute => { const { value } = attribute; if (value === null || value === undefined) { return undefined; } if (isNodeOfType(value, 'Literal')) { if (typeof value.value === 'string') { return value.value; } return value.value === null || value.value === false ? null : undefined; } if (!isNodeOfType(value, 'JSXExpressionContainer')) { return undefined; } const expression = value.expression; if (isNodeOfType(expression, 'Literal')) { if (typeof expression.value === 'string') { return expression.value; } return expression.value === null || expression.value === false ? null : undefined; } if (isNodeOfType(expression, 'Identifier') && expression.name === 'undefined') { return null; } // Only template literals without interpolation are static. A template with // expressions is assumed to include distinguishing context. if (isNodeOfType(expression, 'TemplateLiteral')) { if (expression.expressions.length > 0) { return undefined; } return expression.quasis.map(quasi => { var _quasi$value$cooked; return (_quasi$value$cooked = quasi.value.cooked) !== null && _quasi$value$cooked !== void 0 ? _quasi$value$cooked : ''; }).join(''); } return undefined; }; const rule = createLintRule({ meta: { name: 'no-empty-icon-button-label', type: 'problem', docs: { description: 'Ensures icon-only Atlassian Design System buttons do not have an empty accessible name.', recommended: true, severity: 'warn' }, messages: { emptyLabel: 'The `label` prop is the only accessible name for an icon-only button, so it must not be empty.' } }, create(context) { const iconButtonNames = new Set(); return { ImportDeclaration(node) { const source = String(node.source.value); if (DEFAULT_IMPORT_SOURCES.has(source)) { node.specifiers.forEach(specifier => { if (isNodeOfType(specifier, 'ImportDefaultSpecifier')) { iconButtonNames.add(specifier.local.name); } }); return; } if (NAMED_IMPORT_SOURCES.has(source)) { node.specifiers.forEach(specifier => { if (isNodeOfType(specifier, 'ImportSpecifier') && isNodeOfType(specifier.imported, 'Identifier') && NAMED_IMPORTS.has(specifier.imported.name)) { iconButtonNames.add(specifier.local.name); } }); } }, JSXElement(node) { if (!isNodeOfType(node, 'JSXElement')) { return; } if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) { return; } if (!iconButtonNames.has(node.openingElement.name.name)) { return; } const labelProp = findProp(node, 'label'); // A missing `label` is already a type error, so it is not reported here. if (!labelProp) { return; } const label = getStaticLabel(labelProp); if (label === undefined) { return; } if (label === null || normalize(label) === '') { context.report({ node: labelProp, messageId: 'emptyLabel' }); return; } } }; } }); export default rule;