@atlaskit/eslint-plugin-ui-styling-standard
Version:
The eslint plugin to enforce and educate on Atlassian's UI Styling Standard
158 lines • 6.99 kB
JavaScript
import { getScope, getSourceCode } from '@atlaskit/eslint-utils/context-compat';
import { getImportSources, isCompiled } from '@atlaskit/eslint-utils/is-supported-import';
import { createLintRule } from '../utils/create-rule';
const isTypographyImportSpecifier = def => {
var _def$parent, _def$parent2, _def$parent3;
return def.node.type === 'ImportSpecifier' && def.node.imported.type === 'Identifier' && ((_def$parent = def.parent) === null || _def$parent === void 0 ? void 0 : _def$parent.type) === 'ImportDeclaration' && (def.node.imported.name === 'typography' && ((_def$parent2 = def.parent) === null || _def$parent2 === void 0 ? void 0 : _def$parent2.source.value) === '@atlaskit/theme' || ((_def$parent3 = def.parent) === null || _def$parent3 === void 0 ? void 0 : _def$parent3.source.value) === '@atlaskit/theme/typography');
};
const isElevationImportSpecifier = def => {
var _def$parent4, _def$parent5, _def$parent6;
return def.node.type === 'ImportSpecifier' && def.node.imported.type === 'Identifier' && ((_def$parent4 = def.parent) === null || _def$parent4 === void 0 ? void 0 : _def$parent4.type) === 'ImportDeclaration' && (def.node.imported.name === 'elevation' && ((_def$parent5 = def.parent) === null || _def$parent5 === void 0 ? void 0 : _def$parent5.source.value) === '@atlaskit/theme' || ((_def$parent6 = def.parent) === null || _def$parent6 === void 0 ? void 0 : _def$parent6.source.value) === '@atlaskit/theme/elevation');
};
const isSkeletonShimmerImportSpecifier = def => {
var _def$parent7, _def$parent8, _def$parent9, _def$parent9$source$v;
return def.node.type === 'ImportSpecifier' && def.node.imported.type === 'Identifier' && ((_def$parent7 = def.parent) === null || _def$parent7 === void 0 ? void 0 : _def$parent7.type) === 'ImportDeclaration' && def.node.imported.name === 'skeletonShimmer' && typeof ((_def$parent8 = def.parent) === null || _def$parent8 === void 0 ? void 0 : _def$parent8.source.value) === 'string' && ((_def$parent9 = def.parent) === null || _def$parent9 === void 0 ? void 0 : (_def$parent9$source$v = _def$parent9.source.value) === null || _def$parent9$source$v === void 0 ? void 0 : _def$parent9$source$v.startsWith('@atlaskit/theme'));
};
const bannedImportChecks = [{
messageId: 'usingTypography',
isBanned: isTypographyImportSpecifier
}, {
messageId: 'usingElevation',
isBanned: isElevationImportSpecifier
}, {
messageId: 'usingSkeletonShimmer',
isBanned: isSkeletonShimmerImportSpecifier
}];
function isAstNode(value) {
return typeof value === 'object' && value !== null && 'type' in value && typeof value.type === 'string';
}
function hasBannedThemeImport(sourceCode) {
for (const statement of sourceCode.ast.body) {
if (statement.type !== 'ImportDeclaration' || typeof statement.source.value !== 'string') {
continue;
}
const source = statement.source.value;
if (!source.startsWith('@atlaskit/theme')) {
continue;
}
for (const specifier of statement.specifiers) {
if (specifier.type !== 'ImportSpecifier' || specifier.imported.type !== 'Identifier') {
continue;
}
const {
name
} = specifier.imported;
if (source === '@atlaskit/theme/typography' || source === '@atlaskit/theme/elevation') {
return true;
}
if (source === '@atlaskit/theme' && (name === 'typography' || name === 'elevation')) {
return true;
}
if (name === 'skeletonShimmer') {
return true;
}
}
}
return false;
}
function visitIdentifiers(node, visit) {
if (node.type === 'Identifier' && 'name' in node && typeof node.name === 'string') {
visit(node);
}
for (const key of Object.keys(node)) {
if (key === 'parent') {
continue;
}
const value = node[key];
if (Array.isArray(value)) {
for (const child of value) {
if (isAstNode(child)) {
visitIdentifiers(child, visit);
}
}
} else if (isAstNode(value)) {
visitIdentifiers(value, visit);
}
}
}
function findReferencedVariable(scope, identifier) {
let current = scope;
while (current) {
const variable = current.set.get(identifier.name);
if (variable !== null && variable !== void 0 && variable.references.some(reference => reference.identifier === identifier)) {
return variable;
}
current = current.upper;
}
return undefined;
}
const rule = createLintRule({
meta: {
name: 'atlaskit-theme',
docs: {
description: 'Ban certain usages of `@atlaskit/theme` that `@compiled/react` does not understand',
removeFromPresets: true
},
messages: {
usingTypography: 'Typography does not work with Compiled. Please utilise alternatives like Heading or tokens instead and see https://atlassian.design/components/eslint-plugin-ui-styling-standard/atlaskit-theme/usage',
usingElevation: 'Elevation does not work with Compiled. Please use a token with a fallback instead and see https://atlassian.design/components/eslint-plugin-ui-styling-standard/atlaskit-theme/usage',
usingSkeletonShimmer: 'Skeleton Shimmer does not work with Compiled. Please use an SVG skeleton instead and see https://atlassian.design/components/eslint-plugin-ui-styling-standard/atlaskit-theme/usage'
},
schema: [],
type: 'problem'
},
create(context) {
const sourceCode = getSourceCode(context);
if (!hasBannedThemeImport(sourceCode)) {
return {};
}
const importSources = getImportSources(context);
const reported = new WeakSet();
const reportBannedIdentifier = (identifier, scope) => {
if (identifier.name === 'fontFallback' || reported.has(identifier)) {
return;
}
const variable = findReferencedVariable(scope, identifier);
if (!variable) {
return;
}
let didReport = false;
for (const {
messageId,
isBanned
} of bannedImportChecks) {
if (variable.defs.some(isBanned)) {
context.report({
messageId,
node: identifier
});
didReport = true;
}
}
if (didReport) {
reported.add(identifier);
}
};
const lintCompiledInvocation = node => {
if (node.type !== 'CallExpression' && node.type !== 'TaggedTemplateExpression') {
return;
}
const calleeOrTag = node.type === 'CallExpression' ? node.callee : node.tag;
const scope = getScope(context, node);
if (!isCompiled(calleeOrTag, scope.references, importSources)) {
return;
}
const roots = node.type === 'CallExpression' ? node.arguments : node.quasi.expressions;
for (const root of roots) {
if (isAstNode(root)) {
visitIdentifiers(root, identifier => reportBannedIdentifier(identifier, scope));
}
}
};
return {
CallExpression: lintCompiledInvocation,
TaggedTemplateExpression: lintCompiledInvocation
};
}
});
export default rule;