UNPKG

@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
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;