UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

227 lines (220 loc) 8.15 kB
import { isNodeOfType } from 'eslint-codemod-utils'; import renameMapping from '@atlaskit/tokens/rename-mapping'; import tokenDefaultValues from '@atlaskit/tokens/token-default-values'; import tokens from '@atlaskit/tokens/token-names'; import { getTokenId } from '@atlaskit/tokens/utils/get-token-id'; import { createLintRule } from '../utils/create-lint-rule'; import { isDecendantOfStyleBlock } from '../utils/is-decendant-of-style-block'; import { isDecendantOfStyleJsxAttribute } from '../utils/is-decendant-of-style-jsx-attribute'; import { isToken } from '../utils/is-token'; const rule = createLintRule({ meta: { name: 'no-unsafe-design-token-usage', docs: { description: 'Enforces design token usage is statically and locally analyzable.', recommended: true, severity: 'error' }, fixable: 'code', type: 'problem', messages: { directTokenUsage: `Access the global theme using the token function. \`\`\` import { token } from '@atlaskit/tokens'; token('{{tokenKey}}'); \`\`\` `, staticToken: `Token string should be inlined directly into the function call. \`\`\` token('color.background.blanket'); \`\`\` `, invalidToken: 'The token "{{name}}" does not exist.', tokenRemoved: 'The token "{{name}}" is removed in favour of "{{replacement}}".', tokenIsExperimental: 'The token "{{name}}" is experimental and should not be used directly at this time. It should be replaced by "{{replacement}}".', tokenFallbackEnforced: `Token function requires a fallback, preferably something that best matches the light/default theme in case tokens aren't present. \`\`\` token('color.background.blanket', N500A); \`\`\` `, tokenFallbackRestricted: `Token function must not use a fallback. \`\`\` token('color.background.blanket'); \`\`\` ` }, schema: [{ type: 'object', properties: { shouldEnforceFallbacks: { type: 'boolean' }, fallbackUsage: { enum: ['forced', 'optional', 'none'] }, UNSAFE_ignoreTokens: { type: 'array', items: { type: 'string' } } } }] }, create(context) { // TODO: JFP-2823 - this type cast was added due to Jira's ESLint v9 migration const config = { ...context.options[0] }; if (!config.fallbackUsage) { config.fallbackUsage = config.shouldEnforceFallbacks ? 'forced' : 'none'; } const UNSAFE_ignoreTokens = new Set(config.UNSAFE_ignoreTokens); return { 'TaggedTemplateExpression[tag.name="css"],TaggedTemplateExpression[tag.object.name="styled"]': node => { if (!isNodeOfType(node, 'TaggedTemplateExpression')) { return; } const value = node.quasi.quasis.map(q => q.value.raw).join(''); const tokenKey = isToken(value, tokens); if (tokenKey) { context.report({ messageId: 'directTokenUsage', node, data: { tokenKey } }); return; } }, 'ObjectExpression > Property > Literal': node => { if (node.type !== 'Literal') { return; } if (typeof node.value !== 'string') { return; } if (!isDecendantOfStyleBlock(node) && !isDecendantOfStyleJsxAttribute(node)) { return; } const tokenKey = isToken(node.value, tokens); const isCSSVar = node.value.startsWith('var('); if (tokenKey) { context.report({ messageId: 'directTokenUsage', node, data: { tokenKey }, fix: fixer => isCSSVar ? fixer.replaceText(node, `token('${tokenKey}')`) : null }); return; } }, 'CallExpression:matches([callee.name="token"], [callee.name="getTokenValue"])': node => { if (!isNodeOfType(node, 'CallExpression')) { return; } const isGetTokenValueCall = isNodeOfType(node.callee, 'Identifier') && node.callee.name === 'getTokenValue'; // Skip processing if it's a `getTokenValue` call and config.fallbackUsage is `none` if (isGetTokenValueCall && config.fallbackUsage === 'none') { return; } if (node.arguments.length < 2 && config.fallbackUsage === 'forced') { let fix; if (node.arguments[0].type === 'Literal') { const { value } = node.arguments[0]; const tokenName = value; const fallbackValue = tokenDefaultValues[tokenName] || null; if (fallbackValue) { fix = fixer => fixer.replaceText(node, `${isNodeOfType(node.callee, 'Identifier') ? node.callee.name : 'token'}('${tokenName}', '${fallbackValue}')`); } } context.report({ messageId: 'tokenFallbackEnforced', node, fix }); } else if (node.arguments.length > 1 && config.fallbackUsage === 'none') { if (node.arguments[0].type === 'Literal') { const { value } = node.arguments[0]; /** * This check allows ADS tokens to be passed in via UNSAFE_ignoreTokens and allow fallbacks even if `fallbackUsage` is set to 'none'. * Temporary solution introduced to allow fallbacks for shape tokens in Jira during migration */ if (value && UNSAFE_ignoreTokens.has(value)) { return; } context.report({ messageId: 'tokenFallbackRestricted', node: node.arguments[1], fix: fixer => fixer.replaceText(node, `${isNodeOfType(node.callee, 'Identifier') ? node.callee.name : 'token'}('${value}')`) }); } else { context.report({ messageId: 'tokenFallbackRestricted', node: node.arguments[1] }); } } if (node.arguments[0] && node.arguments[0].type !== 'Literal') { context.report({ messageId: 'staticToken', node }); return; } const tokenKey = node.arguments[0].value; if (!tokenKey) { return; } const deletedMigrationMeta = renameMapping.filter(t => t.state === 'deleted').find(t => getTokenId(t.path) === tokenKey); if (typeof tokenKey === 'string' && deletedMigrationMeta && deletedMigrationMeta.replacement) { const cleanTokenKey = getTokenId(deletedMigrationMeta.replacement); context.report({ messageId: 'tokenRemoved', node, data: { name: tokenKey, replacement: cleanTokenKey }, fix: fixer => fixer.replaceText(node.arguments[0], `'${cleanTokenKey}'`) }); return; } const tokenMeta = renameMapping.filter(t => t.state === 'experimental').find(t => getTokenId(t.path) === tokenKey); const tokenNames = Object.keys(tokens); if (typeof tokenKey === 'string' && tokenMeta && tokenMeta.replacement) { const replacementValue = tokenMeta.replacement; const isReplacementAToken = tokenNames.includes(replacementValue); context.report({ messageId: 'tokenIsExperimental', node, data: { name: tokenKey, replacement: replacementValue }, fix: fixer => isReplacementAToken ? fixer.replaceText(node.arguments[0], `'${replacementValue}'`) : fixer.replaceText(node, `'${replacementValue}'`) }); return; } if (typeof tokenKey !== 'string' || typeof tokenKey === 'string' && !tokens[tokenKey] && !UNSAFE_ignoreTokens.has(tokenKey)) { context.report({ messageId: 'invalidToken', node, data: { name: tokenKey.toString() } }); return; } } }; } }); export default rule;