@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
180 lines (177 loc) • 7.24 kB
JavaScript
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;