UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

180 lines (177 loc) 7.24 kB
import { isNodeOfType } from 'eslint-codemod-utils'; import { createLintRule } from '../utils/create-lint-rule'; import { FORM_PACKAGE, getFormImportLocalNames } from '../utils/get-form-import-local-names'; import { isImportFromPackage } from '../utils/is-import-from-package'; const specialFieldsByImport = { '@atlaskit/checkbox': { component: 'Checkbox', field: 'CheckboxField', local: undefined }, '@atlaskit/range': { component: 'Range', field: 'RangeField', local: undefined }, '@atlaskit/toggle': { component: 'Toggle', field: 'CheckboxField', local: undefined } }; const specialFieldPackages = Object.keys(specialFieldsByImport); export const useCheckboxFieldMessage = 'Convert Field to CheckboxField'; export const useRangeFieldMessage = 'Convert Field to RangeField'; const rule = createLintRule({ meta: { name: 'use-correct-field', type: 'suggestion', fixable: 'code', hasSuggestions: true, docs: { description: 'Ensure makers use appropriate field component for their respective form elements.', recommended: true, severity: 'warn' }, messages: { useCheckboxField: 'Checkbox components should use the `CheckboxField` component', useRangeField: 'Range components should use the `RangeField` component', useCheckboxFieldForToggle: 'Toggle components should use the `CheckboxField` component' } }, create(context) { let fieldImport; let fieldImportSource; const allPackages = []; return { ImportDeclaration(node) { var _formImports$Field; const source = node.source.value; if (typeof source !== 'string') { return; } if (!node.specifiers.length) { return; } const defaultImport = node.specifiers.filter(spec => isNodeOfType(spec, 'ImportDefaultSpecifier')); const matchedSpecialFieldPackage = specialFieldPackages.find(packageName => isImportFromPackage(source, packageName)); if (matchedSpecialFieldPackage) { allPackages.push(node); // set local to local value if (defaultImport.length && isNodeOfType(defaultImport[0], 'ImportDefaultSpecifier') && isNodeOfType(defaultImport[0].local, 'Identifier')) { specialFieldsByImport[matchedSpecialFieldPackage].local = defaultImport[0].local.name; } } const formImports = getFormImportLocalNames(node); if (!((_formImports$Field = formImports.Field) !== null && _formImports$Field !== void 0 && _formImports$Field.length)) { return; } fieldImportSource = source; const matchingSpecifier = node.specifiers.find(spec => { var _formImports$Field2; if (source === FORM_PACKAGE && isNodeOfType(spec, 'ImportSpecifier') && 'name' in spec.imported && spec.imported.name === 'Field') { return true; } return isNodeOfType(spec, 'ImportDefaultSpecifier') && spec.local.name === ((_formImports$Field2 = formImports.Field) === null || _formImports$Field2 === void 0 ? void 0 : _formImports$Field2[0]); }); if (matchingSpecifier !== null && matchingSpecifier !== void 0 && matchingSpecifier.local) { fieldImport = matchingSpecifier.local; } }, JSXElement(node) { if (!isNodeOfType(node, 'JSXElement')) { return; } if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) { return; } const name = node.openingElement.name.name; // if it's not a field import, skip if (!fieldImport || name !== fieldImport.name) { return; } // If no special field packages were imported, exit early. if (!allPackages.length) { return; } const fieldRenderProp = node.children.find(c => isNodeOfType(c, 'JSXExpressionContainer')); if (!fieldRenderProp) { return; } // I'm not early exiting because it doesn't work with ts for some reason if (isNodeOfType(fieldRenderProp, 'JSXExpressionContainer')) { if (!isNodeOfType(fieldRenderProp.expression, 'ArrowFunctionExpression')) { return; } const q = [fieldRenderProp.expression.body]; let found; while (q.length > 0 && !found) { const child = q.pop(); if ('children' in child) { for (const innerChild of child.children) { q.push(innerChild); } } else if (isNodeOfType(child, 'BlockStatement') && isNodeOfType(child.body[0], 'ExpressionStatement')) { q.push(child.body[0].expression); } if (!isNodeOfType(child, 'JSXElement') || !isNodeOfType(child.openingElement.name, 'JSXIdentifier')) { continue; } const elementName = child.openingElement.name.name; for (const importName in specialFieldsByImport) { // if this child is one of the found component names // then break out of the while loop and use the found object const localName = specialFieldsByImport[importName].local; if (localName === elementName) { found = specialFieldsByImport[importName].component; break; } } } if (!found) { return; } // if checkbox is inside of the field's render prop if (found === 'Checkbox' || found === 'Toggle') { const suggestions = fieldImportSource === FORM_PACKAGE ? [{ desc: useCheckboxFieldMessage, fix(fixer) { const fixes = []; fixes.push(fixer.insertTextBefore(fieldImport, 'CheckboxField, ')); fixes.push(fixer.replaceText(node.openingElement.name, 'CheckboxField')); node.closingElement && fixes.push(fixer.replaceText(node.closingElement.name, 'CheckboxField')); return fixes; } }] : []; context.report({ node: node, messageId: found === 'Checkbox' ? 'useCheckboxField' : 'useCheckboxFieldForToggle', ...(suggestions.length ? { suggest: suggestions } : {}) }); } else if (found === 'Range') { const suggestions = fieldImportSource === FORM_PACKAGE ? [{ desc: useRangeFieldMessage, fix(fixer) { const fixes = []; fixes.push(fixer.insertTextBefore(fieldImport, 'RangeField, ')); fixes.push(fixer.replaceText(node.openingElement.name, 'RangeField')); node.closingElement && fixes.push(fixer.replaceText(node.closingElement.name, 'RangeField')); return fixes; } }] : []; context.report({ node: node, messageId: 'useRangeField', ...(suggestions.length ? { suggest: suggestions } : {}) }); } } } }; } }); export default rule;