@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
388 lines (387 loc) • 19.7 kB
JavaScript
"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;