@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
338 lines (325 loc) • 17.4 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.createWithConfig = void 0;
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _eslintCodemodUtils = require("eslint-codemod-utils");
var _contextCompat = require("@atlaskit/eslint-utils/context-compat");
var _isSupportedImport = require("@atlaskit/eslint-utils/is-supported-import");
var _createLintRule = require("../utils/create-lint-rule");
var _errorBoundary = require("../utils/error-boundary");
var _includesHardCodedColor = require("../utils/includes-hard-coded-color");
var _isDecendantOfGlobalToken = require("../utils/is-decendant-of-global-token");
var _isDecendantOfStyleBlock = require("../utils/is-decendant-of-style-block");
var _isDecendantOfType = require("../utils/is-decendant-of-type");
var _isDecendantOfXcssBlock = require("../utils/is-decendant-of-xcss-block");
var _convertHyphenatedNameToCamelCase = require("./convert-hyphenated-name-to-camel-case");
var _emToPixels = require("./em-to-pixels");
var _getDomainsForProperty = require("./get-domains-for-property");
var _getFontSizeValueInScope = require("./get-font-size-value-in-scope");
var _getPropertyNodeFromParent = require("./get-property-node-from-parent");
var _getTokenReplacement = require("./get-token-replacement");
var _getValueForPropertyNode = require("./get-value-for-property-node");
var _getValueFromShorthand = require("./get-value-from-shorthand");
var _getValueFromTemplateLiteralRaw = require("./get-value-from-template-literal-raw");
var _includesTokenString = require("./includes-token-string");
var _insertTokensImport = require("./insert-tokens-import");
var _isAuto = require("./is-auto");
var _isCalc = require("./is-calc");
var _isTokenValueString = require("./is-token-value-string");
var _isValidSpacingValue = require("./is-valid-spacing-value");
var _isZero = require("./is-zero");
var _lintJsxIdentifierForColor = require("./lint-jsx-identifier-for-color");
var _lintJsxLiteralForColor = require("./lint-jsx-literal-for-color");
var _lintJsxMemberForColor = require("./lint-jsx-member-for-color");
var _lintObjectForColor = require("./lint-object-for-color");
var _lintTemplateIdentifierForColor = require("./lint-template-identifier-for-color");
var _processCssNode = require("./process-css-node");
var _ruleMeta = _interopRequireDefault(require("./rule-meta"));
var _spacing = require("./spacing");
var _splitShorthandValues = require("./split-shorthand-values");
var defaultConfig = {
domains: ['color', 'spacing'],
applyImport: true,
shouldEnforceFallbacks: false,
failSilently: false
};
var createWithConfig = exports.createWithConfig = function createWithConfig(initialConfig) {
return function (context) {
// TODO: JFP-2823 - this type cast was added due to Jira's ESLint v9 migration
var userConfig = context.options[0];
// merge configs
var config = {
domains: (userConfig === null || userConfig === void 0 ? void 0 : userConfig.domains) || initialConfig.domains,
applyImport: (userConfig === null || userConfig === void 0 ? void 0 : userConfig.applyImport) !== undefined ? userConfig.applyImport : initialConfig.applyImport,
shouldEnforceFallbacks: (userConfig === null || userConfig === void 0 ? void 0 : userConfig.shouldEnforceFallbacks) !== undefined ? userConfig.shouldEnforceFallbacks : initialConfig.shouldEnforceFallbacks,
exceptions: (userConfig === null || userConfig === void 0 ? void 0 : userConfig.exceptions) || [],
failSilently: (userConfig === null || userConfig === void 0 ? void 0 : userConfig.failSilently) || defaultConfig.failSilently
};
var tokenNode = null;
return (0, _errorBoundary.errorBoundary)({
ImportDeclaration: function ImportDeclaration(node) {
if (node.source.value === '@atlaskit/tokens' && config.applyImport) {
tokenNode = node;
}
},
// For expressions within template literals (e.g. `color: ${red}`) - color only
'TemplateLiteral > Identifier': function TemplateLiteral__Identifier(node) {
if (config.domains.includes('color')) {
return (0, _lintTemplateIdentifierForColor.lintTemplateIdentifierForColor)(node, context, config);
}
return;
},
// const styles = css({ color: 'red', margin: '4px' }), styled.div({ color: 'red', margin: '4px' })
ObjectExpression: function (_ObjectExpression) {
function ObjectExpression(_x) {
return _ObjectExpression.apply(this, arguments);
}
ObjectExpression.toString = function () {
return _ObjectExpression.toString();
};
return ObjectExpression;
}(function (parentNode) {
var _getScope = (0, _contextCompat.getScope)(context, parentNode),
references = _getScope.references;
/**
* NOTE: This rule doesn't have an `importSources` config option,
* so this will just be equal to DEFAULT_IMPORT_SOURCES (which is fine)
*/
var importSources = (0, _isSupportedImport.getImportSources)(context);
// To force the correct node type
if (!(0, _eslintCodemodUtils.isNodeOfType)(parentNode, 'ObjectExpression')) {
return;
}
// Return for nested objects - these get handled automatically so without returning we'd be doubling up
if (parentNode.parent.type === 'Property') {
return;
}
if ((0, _isDecendantOfXcssBlock.isDecendantOfXcssBlock)(parentNode, references, importSources)) {
return;
}
if (!(0, _isDecendantOfStyleBlock.isDecendantOfStyleBlock)(parentNode) && !(0, _isDecendantOfType.isDecendantOfType)(parentNode, 'JSXExpressionContainer')) {
return;
}
function findObjectStyles(node) {
if (!(0, _eslintCodemodUtils.isNodeOfType)(node, 'Property')) {
return;
}
if ((0, _eslintCodemodUtils.isNodeOfType)(node.value, 'ObjectExpression')) {
return node.value.properties.forEach(findObjectStyles);
}
if (!(0, _eslintCodemodUtils.isNodeOfType)(node.key, 'Identifier') && !(0, _eslintCodemodUtils.isNodeOfType)(node.key, 'Literal')) {
return;
}
var propertyName = (0, _eslintCodemodUtils.isNodeOfType)(node.key, 'Identifier') ? node.key.name : String(node.key.value);
// Returns which domains to lint against based on rule's config and current property
var domains = (0, _getDomainsForProperty.getDomainsForProperty)(propertyName, config.domains);
if (domains.length === 0 || (0, _isDecendantOfGlobalToken.isDecendantOfGlobalToken)(node.value)) {
return;
}
if ((0, _eslintCodemodUtils.isNodeOfType)(node.value, 'TemplateLiteral')) {
var value = (0, _getValueFromTemplateLiteralRaw.getValueFromTemplateLiteralRaw)(node.value, context);
if (Array.isArray(value) && value.some(_isCalc.isCalc)) {
return context.report({
node: node,
messageId: 'noCalcUsage',
data: {
payload: "".concat(propertyName)
}
});
}
if (node.value.expressions.some(_isDecendantOfGlobalToken.isDecendantOfGlobalToken)) {
return;
}
}
if (domains.includes('color')) {
return (0, _lintObjectForColor.lintObjectForColor)(node, context, config);
}
if (domains.includes('spacing') || domains.includes('shape')) {
/**
* We do this in case the fontSize for a style object is declared alongside the `em` or `lineHeight` declaration.
*/
var fontSizeNode = (0, _getPropertyNodeFromParent.getPropertyNodeFromParent)('fontSize', parentNode);
var fontSize = fontSizeNode && (0, _getValueForPropertyNode.getValueForPropertyNode)(fontSizeNode, context);
return (0, _spacing.lintObjectForSpacing)(node, context, config, fontSize, tokenNode);
}
}
parentNode.properties.forEach(findObjectStyles);
}),
// CSSTemplateLiteral and StyledTemplateLiteral
// const cssTemplateLiteral = css`color: red; padding: 12px`;
// const styledTemplateLiteral = styled.p`color: red; padding: 8px`;
'TaggedTemplateExpression[tag.name="css"],TaggedTemplateExpression[tag.object.name="styled"],TaggedTemplateExpression[tag.callee.name="styled"]': function TaggedTemplateExpressionTagNameCssTaggedTemplateExpressionTagObjectNameStyledTaggedTemplateExpressionTagCalleeNameStyled(node) {
// To force the correct node type
if (!(0, _eslintCodemodUtils.isNodeOfType)(node, 'TaggedTemplateExpression')) {
return;
}
var processedCssLines = (0, _processCssNode.processCssNode)(node, context);
if (!processedCssLines) {
// if we can't get a processed css we bail
return;
}
var globalFontSize = (0, _getFontSizeValueInScope.getFontSizeValueInScope)(processedCssLines);
var textForSource = (0, _contextCompat.getSourceCode)(context).getText(node.quasi);
var allReplacedValues = [];
var completeSource = processedCssLines.reduce(function (currentSource, _ref) {
var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
resolvedCssLine = _ref2[0],
originalCssLine = _ref2[1];
var _resolvedCssLine$spli = resolvedCssLine.split(':'),
_resolvedCssLine$spli2 = (0, _slicedToArray2.default)(_resolvedCssLine$spli, 2),
originalProperty = _resolvedCssLine$spli2[0],
resolvedCssValues = _resolvedCssLine$spli2[1];
var _originalCssLine$spli = originalCssLine.split(':'),
_originalCssLine$spli2 = (0, _slicedToArray2.default)(_originalCssLine$spli, 2),
_ = _originalCssLine$spli2[0],
originalCssValues = _originalCssLine$spli2[1];
var propertyName = (0, _convertHyphenatedNameToCamelCase.convertHyphenatedNameToCamelCase)(originalProperty);
var isFontFamily = /fontFamily/.test(propertyName);
var replacedValuesPerProperty = [originalProperty];
var domains = (0, _getDomainsForProperty.getDomainsForProperty)(propertyName, config.domains);
if (domains.length === 0 || !resolvedCssValues) {
// in both of these cases no changes should be made to the current property
return currentSource;
}
if (domains.includes('color')) {
if ((0, _includesTokenString.includesTokenString)(resolvedCssValues.trim())) {
return currentSource;
}
if ((0, _includesHardCodedColor.includesHardCodedColor)(resolvedCssValues)) {
context.report({
messageId: 'hardCodedColor',
node: node
});
return currentSource;
}
}
if (domains.includes('spacing') || domains.includes('shape')) {
if (!(0, _isValidSpacingValue.isValidSpacingValue)(resolvedCssValues, globalFontSize)) {
// no changes should be made to the current property
return currentSource;
}
// gets the values from the associated property, numeric values or NaN
var processedNumericValues = (0, _getValueFromShorthand.getValueFromShorthand)(resolvedCssValues);
var processedValues = (0, _splitShorthandValues.splitShorthandValues)(resolvedCssValues);
// only splits shorthand values but it does not transform NaNs so tokens are preserved
var originalValues = (0, _splitShorthandValues.splitShorthandValues)(originalCssValues);
// reconstructing the string
// should replace what it can and preserve the raw value for everything else
var replacementValue = processedNumericValues
// put together resolved value and original value on a tuple
.map(function (value, index) {
return [
// if emToPX conversion fails we'll default to original value
(0, _emToPixels.emToPixels)(value, globalFontSize) || value, processedValues[index], originalValues[index]];
}).map(function (_ref3) {
var _ref4 = (0, _slicedToArray2.default)(_ref3, 3),
numericOrNanValue = _ref4[0],
pxValue = _ref4[1],
originalValue = _ref4[2];
if (!originalValue) {
return originalValue;
}
if ((0, _isCalc.isCalc)(originalValue)) {
context.report({
node: node,
messageId: 'noCalcUsage',
data: {
payload: "".concat(propertyName)
}
});
return originalValue;
}
if ((0, _isTokenValueString.isTokenValueString)(originalValue)) {
// if the value is already valid, nothing to report or replace
return originalValue;
}
// do not replace 0 or auto with tokens
if ((0, _isZero.isZero)(pxValue) || (0, _isAuto.isAuto)(pxValue)) {
return originalValue;
}
if (isNaN(numericOrNanValue) && !isFontFamily) {
// do not report if we have nothing to replace with
return originalValue;
}
// value is numeric or fontFamily, and needs replacing we'll report first
context.report({
node: node,
messageId: 'noRawSpacingValues',
data: {
payload: "".concat(propertyName, ":").concat(numericOrNanValue)
}
});
// from here on we know value is numeric or a font family, so it might or might not have a token equivalent
var replacementNode = (0, _getTokenReplacement.getTokenReplacement)(propertyName, numericOrNanValue);
if (!replacementNode) {
return originalValue;
}
var replacementToken = '${' + replacementNode.toString() + '}';
replacedValuesPerProperty.push(isFontFamily ? numericOrNanValue.trim() : pxValue);
return replacementToken;
}).join(' ');
if (replacedValuesPerProperty.length > 1) {
// first value is the property name, so it will always have at least 1
allReplacedValues.push(replacedValuesPerProperty);
}
// replace property:val with new property:val
var replacedCssLine = currentSource.replace(originalCssLine, // padding: ${gridSize()}px;
"".concat(originalProperty, ": ").concat(replacementValue));
if (!replacedCssLine) {
return currentSource;
}
return replacedCssLine;
}
return currentSource;
}, textForSource);
if (completeSource !== textForSource) {
// means we found some replacement values, we'll give the option to fix them
context.report({
node: node,
messageId: 'autofixesPossible',
fix: function fix(fixer) {
return (!tokenNode && config.applyImport ? [(0, _insertTokensImport.insertTokensImport)(fixer)] : []).concat([fixer.replaceText(node.quasi, completeSource)]);
}
});
}
},
// For inline JSX styles - literals (e.g. <Test color="red"/>) - color only
'JSXAttribute > Literal': function JSXAttribute__Literal(node) {
if (config.domains.includes('color')) {
return (0, _lintJsxLiteralForColor.lintJSXLiteralForColor)(node, context, config);
}
return;
},
// Add handling for JSXExpressionContainer with string literals
'JSXAttribute > JSXExpressionContainer > Literal': function JSXAttribute__JSXExpressionContainer__Literal(node) {
if (config.domains.includes('color')) {
return (0, _lintJsxLiteralForColor.lintJSXLiteralForColor)(node, context, config);
}
return;
},
// For inline JSX styles - members (e.g. <Test color={color.red}/>) - color only
'JSXExpressionContainer > MemberExpression': function JSXExpressionContainer__MemberExpression(node) {
if (config.domains.includes('color')) {
return (0, _lintJsxMemberForColor.lintJSXMemberForColor)(node, context, config);
}
return;
},
// For inline JSX styles - identifiers (e.g. <Test color={red}/>) - color only
'JSXExpressionContainer > Identifier': function JSXExpressionContainer__Identifier(node) {
if (config.domains.includes('color')) {
return (0, _lintJsxIdentifierForColor.lintJSXIdentifierForColor)(node, context, config);
}
return;
}
}, config);
};
};
var rule = (0, _createLintRule.createLintRule)({
meta: _ruleMeta.default,
create: createWithConfig(defaultConfig)
});
// eslint-disable-next-line @atlaskit/volt-strict-mode/no-multiple-exports
var _default = exports.default = rule;