@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
154 lines (153 loc) • 6.44 kB
JavaScript
import { closestOfType, hasImportDeclaration, isNodeOfType } from 'eslint-codemod-utils';
import { getDeclaredVariables, getSourceCode } from '@atlaskit/eslint-utils/context-compat';
import { createLintRule } from '../utils/create-lint-rule';
import { isCssInJsObjectNode } from '../utils/is-css-in-js-object-node';
import { isCssInJsTemplateNode } from '../utils/is-css-in-js-template-node';
import { countMatchingKeyValues } from './count-matching-key-values';
import fixJsx from './fix-jsx';
import fixVanilla from './fix-vanilla';
import { getObjectLikeness } from './get-object-likeness';
import { makeTemplateLiteralIntoEntries } from './make-template-literal-into-entries';
const THEME_IMPORT_NAMES = ['visuallyHidden', 'assistive'];
const rule = createLintRule({
meta: {
name: 'use-visually-hidden',
type: 'suggestion',
fixable: 'code',
docs: {
description: 'Enforce usage of the visually hidden component.',
recommended: true,
severity: 'error'
},
messages: {
noDeprecatedUsage: 'Using the export `{{local}}` from `{{import}}` as a mixin is discouraged. Please use `@atlaskit/visually-hidden` instead.',
noDeprecated: 'The export `{{local}}` from `{{import}}` is deprecated. Please use `@atlaskit/visually-hidden` instead.',
suggestion: 'This CSS closely matches the implementation of a visually hidden element. You should consider using the `@atlaskit/visually-hidden` component instead.'
}
},
create(context) {
const source = getSourceCode(context);
return {
ImportDeclaration(node) {
const isThemeNode = hasImportDeclaration(node, '@atlaskit/theme') || hasImportDeclaration(node, '@atlaskit/theme/constants');
if (!isThemeNode) {
return;
}
const visuallyHiddenOrAssistive = node.specifiers.filter(specifier => specifier.type === 'ImportSpecifier').find(specifier => 'name' in specifier.imported && THEME_IMPORT_NAMES.includes(specifier.imported.name));
if (!visuallyHiddenOrAssistive) {
return;
}
getDeclaredVariables(context, visuallyHiddenOrAssistive).forEach(someNode => {
someNode.references.map(innerNode => innerNode.identifier).forEach(idNode => {
// @ts-ignore JSX is not typed correctly in eslint
if ((idNode === null || idNode === void 0 ? void 0 : idNode.parent.type) === 'JSXExpressionContainer') {
context.report({
node: idNode.parent,
messageId: 'noDeprecatedUsage',
data: {
import: `${node.source.value}`,
local: visuallyHiddenOrAssistive.local.name
},
fix: fixJsx(source, idNode)
});
// this is either a styled usage OR mixin usage in a styled usage
} else if (idNode.parent.type === 'CallExpression') {
if (isCssInJsObjectNode(idNode.parent) || isCssInJsTemplateNode(idNode.parent)) {
context.report({
node: idNode.parent,
messageId: 'noDeprecatedUsage',
data: {
import: `${node.source.value}`,
local: visuallyHiddenOrAssistive.local.name
},
fix: fixVanilla(source, idNode.parent)
});
}
if (idNode.parent.callee === idNode) {
context.report({
node: idNode.parent,
messageId: 'noDeprecatedUsage',
data: {
import: `${node.source.value}`,
local: visuallyHiddenOrAssistive.local.name
},
fix: fixVanilla(source, closestOfType(idNode.parent, 'TaggedTemplateExpression'))
});
}
}
});
});
return context.report({
node: visuallyHiddenOrAssistive,
messageId: 'noDeprecated',
data: {
import: `${node.source.value}`,
local: visuallyHiddenOrAssistive.local.name
}
});
},
CallExpression(node) {
var _node$arguments$;
if (node.type !== 'CallExpression') {
return;
}
if (!(node.callee.type === 'MemberExpression' || node.callee.type === 'Identifier')) {
return;
}
const isStyled = isCssInJsObjectNode(node);
if (node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name !== 'styled') {
return;
}
if (node.callee.type === 'Identifier' && node.callee.name !== 'css') {
return;
}
// This is an object style (probably)
if (node.arguments && ((_node$arguments$ = node.arguments[0]) === null || _node$arguments$ === void 0 ? void 0 : _node$arguments$.type) === 'ObjectExpression') {
const matchingScore = getObjectLikeness(node.arguments[0]);
if (matchingScore > 0.8) {
return context.report({
node: node.parent,
messageId: 'suggestion',
fix: isStyled ? fixVanilla(source, node) : undefined
});
}
}
return null;
},
ObjectExpression(node) {
if (node.parent.type === 'CallExpression') {
return;
}
const matchingScore = getObjectLikeness(node);
if (matchingScore > 0.8) {
return context.report({
node: node,
messageId: 'suggestion'
});
}
},
'TaggedTemplateExpression[tag.name="css"],TaggedTemplateExpression[tag.object.name="styled"]': node => {
if (!isNodeOfType(node, 'TaggedTemplateExpression')) {
return;
}
const templateString = node.quasi.quasis.map(q => q.value.raw).join('');
const styleEntries = makeTemplateLiteralIntoEntries(templateString);
if (!styleEntries) {
return;
}
const count = countMatchingKeyValues(styleEntries.map(([key, value]) => ({
key,
value
})));
if (count > 0.8) {
return context.report({
node,
messageId: 'suggestion',
fix: node.tag.type !== 'Identifier' ? fixVanilla(source, node) : undefined
});
}
}
};
}
});
export default rule;