UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

388 lines (387 loc) 19.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _contextCompat = require("@atlaskit/eslint-utils/context-compat"); var _root = require("../../ast-nodes/root"); var _createLintRule = require("../utils/create-lint-rule"); var COMPILED_PRIMITIVES = '@atlaskit/primitives/compiled'; var COMPILED_PRESSABLE = '@atlaskit/primitives/compiled/pressable'; var LEGACY_PRESSABLE = '@atlaskit/primitives/pressable'; var XCSS = '@atlaskit/primitives/xcss'; var ATLASKIT_CSS = '@atlaskit/css'; var COMPILED_REACT = '@compiled/react'; var TOKENS = '@atlaskit/tokens'; var BUTTON_PROPERTIES = new Set(['backgroundColor', 'background']); var LIST_ITEM_PROPERTIES = new Set(['backgroundColor', 'background', 'borderColor', 'color', 'textDecorationColor']); var CSS_SELECTORS = { hover: '&:hover', active: '&:active' }; var XCSS_SELECTORS = { hover: ':hover', active: ':active' }; var getPropertyName = function getPropertyName(property) { if (property.computed) { return; } if (property.key.type === 'Identifier') { return property.key.name; } return property.key.type === 'Literal' && typeof property.key.value === 'string' ? property.key.value : undefined; }; var getProperty = function getProperty(object, name) { return object.properties.find(function (property) { return property.type === 'Property' && getPropertyName(property) === name; }); }; var getNestedStyle = function getNestedStyle(base, selector) { var property = getProperty(base, selector); return (property === null || property === void 0 ? void 0 : property.value.type) === 'ObjectExpression' ? property.value : undefined; }; var createStyleRecord = function createStyleRecord(base) { var selectors = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : CSS_SELECTORS; return { base: base, hover: getNestedStyle(base, selectors.hover), active: getNestedStyle(base, selectors.active) }; }; var hasSpread = function hasSpread(object) { return object.properties.some(function (property) { return property.type === 'SpreadElement'; }); }; var isColorOnlyBackground = function isColorOnlyBackground(property, tokenNames) { var _property$value$argum; if (getPropertyName(property) !== 'background') { return false; } if (property.value.type === 'Literal' && typeof property.value.value === 'string') { var value = property.value.value.trim().toLowerCase(); return !/\b(url|image|gradient|var)\s*\(/.test(value) && !value.includes(','); } return property.value.type === 'CallExpression' && property.value.callee.type === 'Identifier' && tokenNames.has(property.value.callee.name) && ((_property$value$argum = property.value.arguments[0]) === null || _property$value$argum === void 0 ? void 0 : _property$value$argum.type) === 'Literal' && typeof property.value.arguments[0].value === 'string' && property.value.arguments[0].value.startsWith('color.'); }; var hasRelevantColorChange = function hasRelevantColorChange(object, properties, tokenNames) { if (!object || hasSpread(object)) { return false; } return object.properties.some(function (property) { if (property.type !== 'Property') { return false; } var name = getPropertyName(property); return name === 'background' && isColorOnlyBackground(property, tokenNames) || name !== undefined && properties.has(name) && name !== 'background'; }); }; var getIndent = function getIndent(context, node) { var _sourceCode$lines, _node$loc$start$line, _node$loc, _line$match$, _line$match; var sourceCode = (0, _contextCompat.getSourceCode)(context); var line = (_sourceCode$lines = sourceCode.lines[((_node$loc$start$line = (_node$loc = node.loc) === null || _node$loc === void 0 ? void 0 : _node$loc.start.line) !== null && _node$loc$start$line !== void 0 ? _node$loc$start$line : 1) - 1]) !== null && _sourceCode$lines !== void 0 ? _sourceCode$lines : ''; return (_line$match$ = (_line$match = line.match(/^\s*/)) === null || _line$match === void 0 ? void 0 : _line$match[0]) !== null && _line$match$ !== void 0 ? _line$match$ : ''; }; var insertTransition = function insertTransition(context, fixer, object, value) { var _object$loc, _lastProperty$loc; var lastProperty = object.properties[object.properties.length - 1]; var isInlineObject = ((_object$loc = object.loc) === null || _object$loc === void 0 ? void 0 : _object$loc.start.line) === ((_lastProperty$loc = lastProperty.loc) === null || _lastProperty$loc === void 0 ? void 0 : _lastProperty$loc.start.line); return fixer.insertTextAfter(lastProperty, isInlineObject ? ", transition: ".concat(value) : ",\n".concat(getIndent(context, lastProperty), "transition: ").concat(value)); }; var isExpectedTransition = function isExpectedTransition(property, tokenNames, tokenName) { var _property$value$argum2; return (property === null || property === void 0 ? void 0 : property.value.type) === 'CallExpression' && property.value.callee.type === 'Identifier' && tokenNames.has(property.value.callee.name) && ((_property$value$argum2 = property.value.arguments[0]) === null || _property$value$argum2 === void 0 ? void 0 : _property$value$argum2.type) === 'Literal' && property.value.arguments[0].value === tokenName; }; var rule = (0, _createLintRule.createLintRule)({ meta: { name: 'use-pressable-motion', hasSuggestions: true, type: 'suggestion', docs: { description: 'Suggests semantic motion tokens when Pressable or native buttons change interactive colours through local static styles.', removeFromPresets: true }, messages: { missingPressableMotion: 'An interactive element changes colour properties without the matching semantic motion token.', missingPressableMotionManual: 'An interactive element changes colour properties without the matching semantic motion token. Update the existing transition manually.', useButtonMotion: 'Add button motion tokens for hover and pressed states.', useListItemMotion: 'Add list-item motion tokens for hover and pressed states.' } }, create: function create(context) { var pressableNames = new Set(); var cssNames = new Set(); var cssMapNames = new Set(); var xcssNames = new Set(); var tokenNames = new Set(); var styles = new Map(); var styleUsages = []; var _collectStyleUsages = function collectStyleUsages(node, expression, arrayGroup) { if (expression.type === 'Identifier') { styleUsages.push({ node: node, styleName: expression.name, styleKey: 'default', arrayGroup: arrayGroup }); return; } if (expression.type === 'MemberExpression' && !expression.computed && expression.object.type === 'Identifier' && expression.property.type === 'Identifier') { styleUsages.push({ node: node, styleName: expression.object.name, styleKey: expression.property.name, arrayGroup: arrayGroup }); return; } if (expression.type === 'ArrayExpression') { expression.elements.forEach(function (element) { if (element !== null && element.type !== 'SpreadElement') { _collectStyleUsages(node, element, expression); } }); return; } if (expression.type === 'LogicalExpression') { _collectStyleUsages(node, expression.right, arrayGroup); return; } if (expression.type === 'ConditionalExpression') { _collectStyleUsages(node, expression.consequent, arrayGroup); _collectStyleUsages(node, expression.alternate, arrayGroup); } }; var report = function report(node, style, groupProvidesButtonHoverTransition, groupProvidesListItemHoverTransition, groupProvidesButtonActiveTransition, groupProvidesListItemActiveTransition) { var hasHover = hasRelevantColorChange(style.hover, BUTTON_PROPERTIES, tokenNames); var hasActive = hasRelevantColorChange(style.active, BUTTON_PROPERTIES, tokenNames); var hasListItemHover = hasRelevantColorChange(style.hover, LIST_ITEM_PROPERTIES, tokenNames); var hasListItemActive = hasRelevantColorChange(style.active, LIST_ITEM_PROPERTIES, tokenNames); if (!hasListItemHover && !hasListItemActive) { return; } var baseTransition = getProperty(style.base, 'transition'); var activeTransition = style.active ? getProperty(style.active, 'transition') : undefined; var hasButtonHoverTransition = isExpectedTransition(baseTransition, tokenNames, 'motion.button.hovered') || groupProvidesButtonHoverTransition; var hasButtonActiveTransition = isExpectedTransition(activeTransition, tokenNames, 'motion.button.pressed') || groupProvidesButtonActiveTransition; var hasListItemHoverTransition = isExpectedTransition(baseTransition, tokenNames, 'motion.listitem.hovered') || groupProvidesListItemHoverTransition; var hasListItemActiveTransition = isExpectedTransition(activeTransition, tokenNames, 'motion.listitem.pressed') || groupProvidesListItemActiveTransition; var groupProvidesHoverTransition = hasHover ? groupProvidesButtonHoverTransition || groupProvidesListItemHoverTransition : groupProvidesListItemHoverTransition; var groupProvidesActiveTransition = hasActive ? groupProvidesButtonActiveTransition || groupProvidesListItemActiveTransition : groupProvidesListItemActiveTransition; var needsHover = hasListItemHover && !baseTransition && !groupProvidesHoverTransition; var needsActive = hasListItemActive && !activeTransition && !groupProvidesActiveTransition; var hasUnsafeTransition = hasListItemHover && baseTransition !== undefined && !hasButtonHoverTransition && !hasListItemHoverTransition || hasListItemActive && activeTransition !== undefined && !hasButtonActiveTransition && !hasListItemActiveTransition; if (hasUnsafeTransition) { context.report({ node: node, messageId: 'missingPressableMotionManual' }); return; } if (!needsHover && !needsActive) { return; } var createSuggestion = function createSuggestion(family) { return { messageId: family === 'button' ? 'useButtonMotion' : 'useListItemMotion', fix: function fix(fixer) { var _tokenNames$values$ne; var fixes = []; var tokenIdentifier = (_tokenNames$values$ne = tokenNames.values().next().value) !== null && _tokenNames$values$ne !== void 0 ? _tokenNames$values$ne : 'token'; if (needsHover) { fixes.push(insertTransition(context, fixer, style.base, "".concat(tokenIdentifier, "('motion.").concat(family, ".hovered')"))); } if (needsActive && style.active) { fixes.push(insertTransition(context, fixer, style.active, "".concat(tokenIdentifier, "('motion.").concat(family, ".pressed')"))); } if (tokenNames.size === 0) { var importFix = _root.Root.upsertNamedImportDeclaration({ module: TOKENS, specifiers: ['token'] }, context, fixer); if (importFix) { fixes.push(importFix); } } return fixes; } }; }; var suggestions = [].concat((0, _toConsumableArray2.default)(hasHover || hasActive ? [createSuggestion('button')] : []), [createSuggestion('listitem')]); context.report({ node: node, messageId: 'missingPressableMotion', suggest: suggestions }); }; var providesButtonHoverTransition = function providesButtonHoverTransition(style) { var baseTransition = getProperty(style.base, 'transition'); return isExpectedTransition(baseTransition, tokenNames, 'motion.button.hovered'); }; var providesListItemHoverTransition = function providesListItemHoverTransition(style) { var baseTransition = getProperty(style.base, 'transition'); return isExpectedTransition(baseTransition, tokenNames, 'motion.listitem.hovered'); }; var providesButtonActiveTransition = function providesButtonActiveTransition(style) { if (!style.active) { return false; } var activeTransition = getProperty(style.active, 'transition'); return isExpectedTransition(activeTransition, tokenNames, 'motion.button.pressed'); }; var providesListItemActiveTransition = function providesListItemActiveTransition(style) { if (!style.active) { return false; } var activeTransition = getProperty(style.active, 'transition'); return isExpectedTransition(activeTransition, tokenNames, 'motion.listitem.pressed'); }; return { ImportDeclaration: function ImportDeclaration(node) { if (node.type !== 'ImportDeclaration') { return; } if (node.source.value === COMPILED_PRIMITIVES || node.source.value === COMPILED_PRESSABLE || node.source.value === LEGACY_PRESSABLE) { node.specifiers.forEach(function (specifier) { if (specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'Pressable') { pressableNames.add(specifier.local.name); } }); } if (node.source.value === ATLASKIT_CSS || node.source.value === COMPILED_REACT) { node.specifiers.forEach(function (specifier) { if (specifier.type !== 'ImportSpecifier' || specifier.imported.type !== 'Identifier') { return; } if (specifier.imported.name === 'css') cssNames.add(specifier.local.name); if (specifier.imported.name === 'cssMap') cssMapNames.add(specifier.local.name); }); } if (node.source.value === XCSS) { node.specifiers.forEach(function (specifier) { if (specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'xcss') { xcssNames.add(specifier.local.name); } }); } if (node.source.value === TOKENS) { node.specifiers.forEach(function (specifier) { if (specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier' && specifier.imported.name === 'token') { tokenNames.add(specifier.local.name); } }); } }, VariableDeclarator: function VariableDeclarator(node) { var _node$init; if (node.type !== 'VariableDeclarator') { return; } if (node.id.type !== 'Identifier' || ((_node$init = node.init) === null || _node$init === void 0 ? void 0 : _node$init.type) !== 'CallExpression' || node.init.callee.type !== 'Identifier' || node.init.arguments.length !== 1 || node.init.arguments[0].type !== 'ObjectExpression') { return; } var styleObject = node.init.arguments[0]; if (hasSpread(styleObject)) { return; } if (cssNames.has(node.init.callee.name)) { styles.set(node.id.name, { styles: new Map([['default', createStyleRecord(styleObject)]]) }); return; } if (xcssNames.has(node.init.callee.name)) { styles.set(node.id.name, { styles: new Map([['default', createStyleRecord(styleObject, XCSS_SELECTORS)]]) }); return; } if (!cssMapNames.has(node.init.callee.name)) { return; } var cssMapStyles = new Map(); styleObject.properties.forEach(function (property) { if (property.type !== 'Property' || property.value.type !== 'ObjectExpression') { return; } var key = getPropertyName(property); if (key && !hasSpread(property.value)) { cssMapStyles.set(key, createStyleRecord(property.value)); } }); styles.set(node.id.name, { styles: cssMapStyles }); }, JSXElement: function JSXElement(node) { var _styleAttribute$value; if (node.type !== 'JSXElement') { return; } if (node.openingElement.name.type !== 'JSXIdentifier') { return; } var isPressable = pressableNames.has(node.openingElement.name.name); var isNativeButton = node.openingElement.name.name === 'button'; if (!isPressable && !isNativeButton) { return; } var styleAttribute = node.openingElement.attributes.find(function (attribute) { return attribute.type === 'JSXAttribute' && attribute.name.name === (isNativeButton ? 'css' : 'xcss'); }); if ((styleAttribute === null || styleAttribute === void 0 ? void 0 : styleAttribute.type) !== 'JSXAttribute' || ((_styleAttribute$value = styleAttribute.value) === null || _styleAttribute$value === void 0 ? void 0 : _styleAttribute$value.type) !== 'JSXExpressionContainer') { return; } var expression = styleAttribute.value.expression; if (expression.type !== 'JSXEmptyExpression') { _collectStyleUsages(styleAttribute, expression); } }, 'Program:exit': function ProgramExit() { var groups = new Map(); styleUsages.forEach(function (usage) { if (!usage.arrayGroup) { var _styles$get; var style = (_styles$get = styles.get(usage.styleName)) === null || _styles$get === void 0 ? void 0 : _styles$get.styles.get(usage.styleKey); if (style) { report(usage.node, style, false, false, false, false); } return; } var existing = groups.get(usage.arrayGroup); if (existing) { existing.push(usage); } else { groups.set(usage.arrayGroup, [usage]); } }); groups.forEach(function (usages) { var groupStyles = usages.map(function (usage) { var _styles$get2; return (_styles$get2 = styles.get(usage.styleName)) === null || _styles$get2 === void 0 ? void 0 : _styles$get2.styles.get(usage.styleKey); }).filter(function (style) { return style !== undefined; }); var groupProvidesButtonHoverTransition = groupStyles.some(providesButtonHoverTransition); var groupProvidesListItemHoverTransition = groupStyles.some(providesListItemHoverTransition); var groupProvidesButtonActiveTransition = groupStyles.some(providesButtonActiveTransition); var groupProvidesListItemActiveTransition = groupStyles.some(providesListItemActiveTransition); usages.forEach(function (_ref) { var _styles$get3; var node = _ref.node, styleName = _ref.styleName, styleKey = _ref.styleKey; var style = (_styles$get3 = styles.get(styleName)) === null || _styles$get3 === void 0 ? void 0 : _styles$get3.styles.get(styleKey); if (style) { report(node, style, groupProvidesButtonHoverTransition, groupProvidesListItemHoverTransition, groupProvidesButtonActiveTransition, groupProvidesListItemActiveTransition); } }); }); } }; } }); var _default = exports.default = rule;