UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

222 lines (220 loc) 9.64 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } 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'; var 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.\n\n```\nimport { token } from '@atlaskit/tokens';\n\ntoken('{{tokenKey}}');\n```\n", staticToken: "Token string should be inlined directly into the function call.\n\n```\ntoken('color.background.blanket');\n```\n", 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.\n\n```\ntoken('color.background.blanket', N500A);\n```\n ", tokenFallbackRestricted: "Token function must not use a fallback.\n\n```\ntoken('color.background.blanket');\n```\n " }, schema: [{ type: 'object', properties: { shouldEnforceFallbacks: { type: 'boolean' }, fallbackUsage: { enum: ['forced', 'optional', 'none'] }, UNSAFE_ignoreTokens: { type: 'array', items: { type: 'string' } } } }] }, create: function create(context) { // TODO: JFP-2823 - this type cast was added due to Jira's ESLint v9 migration var config = _objectSpread({}, context.options[0]); if (!config.fallbackUsage) { config.fallbackUsage = config.shouldEnforceFallbacks ? 'forced' : 'none'; } var UNSAFE_ignoreTokens = new Set(config.UNSAFE_ignoreTokens); return { 'TaggedTemplateExpression[tag.name="css"],TaggedTemplateExpression[tag.object.name="styled"]': function TaggedTemplateExpressionTagNameCssTaggedTemplateExpressionTagObjectNameStyled(node) { if (!isNodeOfType(node, 'TaggedTemplateExpression')) { return; } var value = node.quasi.quasis.map(function (q) { return q.value.raw; }).join(''); var tokenKey = isToken(value, tokens); if (tokenKey) { context.report({ messageId: 'directTokenUsage', node: node, data: { tokenKey: tokenKey } }); return; } }, 'ObjectExpression > Property > Literal': function ObjectExpression__Property__Literal(node) { if (node.type !== 'Literal') { return; } if (typeof node.value !== 'string') { return; } if (!isDecendantOfStyleBlock(node) && !isDecendantOfStyleJsxAttribute(node)) { return; } var tokenKey = isToken(node.value, tokens); var isCSSVar = node.value.startsWith('var('); if (tokenKey) { context.report({ messageId: 'directTokenUsage', node: node, data: { tokenKey: tokenKey }, fix: function fix(fixer) { return isCSSVar ? fixer.replaceText(node, "token('".concat(tokenKey, "')")) : null; } }); return; } }, 'CallExpression:matches([callee.name="token"], [callee.name="getTokenValue"])': function CallExpressionMatchesCalleeNameToken_CalleeNameGetTokenValue(node) { if (!isNodeOfType(node, 'CallExpression')) { return; } var 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') { var fix; if (node.arguments[0].type === 'Literal') { var value = node.arguments[0].value; var tokenName = value; var fallbackValue = tokenDefaultValues[tokenName] || null; if (fallbackValue) { fix = function fix(fixer) { return fixer.replaceText(node, "".concat(isNodeOfType(node.callee, 'Identifier') ? node.callee.name : 'token', "('").concat(tokenName, "', '").concat(fallbackValue, "')")); }; } } context.report({ messageId: 'tokenFallbackEnforced', node: node, fix: fix }); } else if (node.arguments.length > 1 && config.fallbackUsage === 'none') { if (node.arguments[0].type === 'Literal') { var _value = node.arguments[0].value; /** * 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: function fix(fixer) { return fixer.replaceText(node, "".concat(isNodeOfType(node.callee, 'Identifier') ? node.callee.name : 'token', "('").concat(_value, "')")); } }); } else { context.report({ messageId: 'tokenFallbackRestricted', node: node.arguments[1] }); } } if (node.arguments[0] && node.arguments[0].type !== 'Literal') { context.report({ messageId: 'staticToken', node: node }); return; } var tokenKey = node.arguments[0].value; if (!tokenKey) { return; } var deletedMigrationMeta = renameMapping.filter(function (t) { return t.state === 'deleted'; }).find(function (t) { return getTokenId(t.path) === tokenKey; }); if (typeof tokenKey === 'string' && deletedMigrationMeta && deletedMigrationMeta.replacement) { var cleanTokenKey = getTokenId(deletedMigrationMeta.replacement); context.report({ messageId: 'tokenRemoved', node: node, data: { name: tokenKey, replacement: cleanTokenKey }, fix: function fix(fixer) { return fixer.replaceText(node.arguments[0], "'".concat(cleanTokenKey, "'")); } }); return; } var tokenMeta = renameMapping.filter(function (t) { return t.state === 'experimental'; }).find(function (t) { return getTokenId(t.path) === tokenKey; }); var tokenNames = Object.keys(tokens); if (typeof tokenKey === 'string' && tokenMeta && tokenMeta.replacement) { var replacementValue = tokenMeta.replacement; var isReplacementAToken = tokenNames.includes(replacementValue); context.report({ messageId: 'tokenIsExperimental', node: node, data: { name: tokenKey, replacement: replacementValue }, fix: function fix(fixer) { return isReplacementAToken ? fixer.replaceText(node.arguments[0], "'".concat(replacementValue, "'")) : fixer.replaceText(node, "'".concat(replacementValue, "'")); } }); return; } if (typeof tokenKey !== 'string' || typeof tokenKey === 'string' && !tokens[tokenKey] && !UNSAFE_ignoreTokens.has(tokenKey)) { context.report({ messageId: 'invalidToken', node: node, data: { name: tokenKey.toString() } }); return; } } }; } }); export default rule;