@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
160 lines (150 loc) • 6.02 kB
JavaScript
import { isNodeOfType, property } from 'eslint-codemod-utils';
import { emToPixels } from './em-to-pixels';
import { findTokenNameByPropertyValue } from './find-token-name-by-property-value';
import { getRawExpression } from './get-raw-expression';
import { getTokenNodeForValue } from './get-token-node-for-value';
import { getTokenReplacement } from './get-token-replacement';
import { getValue } from './get-value';
import { insertTokensImport } from './insert-tokens-import';
import { isAuto } from './is-auto';
import { isCalc } from './is-calc';
import { isValidSpacingValue } from './is-valid-spacing-value';
import { isZero } from './is-zero';
import { splitShorthandValues } from './split-shorthand-values';
export const lintObjectForSpacing = (node, context, ruleConfig, fontSize, tokenNode) => {
if (isNodeOfType(node.value, 'Literal') && !isValidSpacingValue(node.value.value, fontSize)) {
context.report({
node,
messageId: 'noRawSpacingValues',
data: {
payload: `NaN:${node.value.value}`
}
});
return;
}
const propertyName = node.key.name;
const isFontFamily = /fontFamily/.test(propertyName);
// Report on CSS calc function for strings
if (isNodeOfType(node.value, 'Literal') && isCalc(node.value.value)) {
return context.report({
node,
messageId: 'noCalcUsage',
data: {
payload: `${propertyName}`
}
});
}
const value = getValue(node.value, context);
// Value is a token string (e.g. set via a variable)
if (typeof value === 'string' && /\${token\(.*\)}/.test(value)) {
return;
}
// value is either NaN or it can't be resolved (e.g. em, 100% etc...)
if (!(value && isValidSpacingValue(value, fontSize))) {
return context.report({
node,
messageId: 'noRawSpacingValues',
data: {
payload: `NaN:${value}`
}
});
}
// The corresponding values for a single CSS property (e.g. padding: '8px 16px 2px' => [8, 16, 2])
const valuesForProperty = Array.isArray(value) ? value : [value];
// value is a single value so we can apply a more robust approach to our fix
// treat fontFamily as having one value
if (valuesForProperty.length === 1 || isFontFamily) {
const [value] = valuesForProperty;
// Do not report or suggest a token to replace 0 or auto
if (isZero(value) || isAuto(value)) {
return;
}
const pixelValue = isFontFamily ? value : emToPixels(value, fontSize);
return context.report({
node,
messageId: 'noRawSpacingValues',
data: {
payload: `${propertyName}:${pixelValue}`
},
fix: fixer => {
// Casting due to possibility of pixelValue being string | number from emToPixels
const replacementNode = pixelValue && getTokenReplacement(propertyName, pixelValue);
if (!replacementNode) {
return null;
}
return (!tokenNode && ruleConfig.applyImport ? [insertTokensImport(fixer)] : []).concat([fixer.replaceText(node, property({
...node,
value: replacementNode
}).toString())]);
}
});
}
/**
* Compound values are hard to deal with / replace because we need to find/replace strings inside an
* estree node.
*
* @example
* { padding: '8px 0px' }
*/
valuesForProperty.forEach(val => {
if (isCalc(val)) {
return context.report({
node,
messageId: 'noCalcUsage',
data: {
payload: `${propertyName}:${val}`
}
});
}
const pixelValue = emToPixels(val, fontSize);
// Do not report or suggest a token to replace 0 or auto
if (isZero(val) || isAuto(val)) {
return;
}
context.report({
node,
messageId: 'noRawSpacingValues',
data: {
payload: `${propertyName}:${pixelValue}`
},
fix: fixer => {
const allResolvableValues = valuesForProperty.every(value => !Number.isNaN(emToPixels(value, fontSize)));
if (!allResolvableValues) {
return null;
}
// Casting due to possibility of value being string | number
const valuesWithTokenReplacement = valuesForProperty.filter(value => findTokenNameByPropertyValue(propertyName, value)).filter(value => value !== 0);
if (valuesWithTokenReplacement.length === 0) {
// all values could be replaceable but that just means they're numeric
// if none of the values have token replacement we bail
return null;
}
const originalCssString = getRawExpression(node.value, context);
if (!originalCssString) {
return null;
}
/**
* at this stage none of the values are tokens or irreplaceable values
* since we know this is shorthand, there will be multiple values
* we'll need to remove all quote chars since `getRawExpression` will return those as part of the string
* given they're part of the substring of the current node
*/
const originalValues = splitShorthandValues(originalCssString.replace(/`|'|"/g, ''));
if (originalValues.length !== valuesForProperty.length) {
// we bail just in case original values don't correspond to the replaced values
return null;
}
return (!tokenNode && ruleConfig.applyImport ? [insertTokensImport(fixer)] : []).concat([fixer.replaceText(node.value, `\`${valuesForProperty.map((value, index) => {
if (isZero(value)) {
return originalValues[index];
}
const pixelValue = emToPixels(value, fontSize);
const pixelValueString = `${pixelValue}px`;
// if there is a token we take it, otherwise we go with the original value
// Casting due to possibility of value being string | number
return findTokenNameByPropertyValue(propertyName, value) ? `\${${getTokenNodeForValue(propertyName, pixelValueString)}}` : originalValues[index];
}).join(' ')}\``)]);
}
});
});
};