@atlaskit/eslint-plugin-ui-styling-standard
Version:
The eslint plugin to enforce and educate on Atlassian's UI Styling Standard
149 lines (147 loc) • 5.38 kB
JavaScript
import { getSourceCode } from '@atlaskit/eslint-utils/context-compat';
import { importSources } from '@atlaskit/eslint-utils/schema';
import { createLintRuleWithTypedConfig } from '../utils/create-rule-with-typed-config';
import { isTypeWrapper } from '../utils/is-type-wrapper';
import { getStyleCalls } from '../utils/style-calls';
var rule = createLintRuleWithTypedConfig({
meta: {
name: 'no-important-styles',
docs: {
description: 'Disallows important style declarations',
recommended: true,
severity: 'error'
},
messages: {
'no-important-styles': "Remove !important and apply the intended style directly to the owning element; resolve conflicting declarations through local style composition."
},
type: 'problem',
schema: {
type: 'object',
properties: {
importSources: importSources
}
}
},
create: function create(context, _ref) {
var importSources = _ref.importSources;
var sourceCode = getSourceCode(context);
if (!sourceCode.text.includes('important')) {
return {};
}
return {
Program: function Program() {
var text = sourceCode.text;
var reportedValues = new Set();
var importantIndex = text.indexOf('important');
while (importantIndex !== -1) {
var value = findStyleValue(sourceCode, importantIndex);
if (value && !reportedValues.has(value) && isImportant(value) && isInSupportedStyleCall(context, value, importSources)) {
reportedValues.add(value);
context.report({
node: value,
messageId: 'no-important-styles'
});
}
importantIndex = text.indexOf('important', importantIndex + 9);
}
}
};
}
});
export default rule;
function isImportant(node) {
if (node.type === 'Literal') {
return typeof node.value === 'string' && hasImportantSuffix(node.value);
}
if (node.type === 'TemplateLiteral') {
var lastQuasi = node.quasis[node.quasis.length - 1];
return Boolean(lastQuasi && hasImportantSuffix(lastQuasi.value.raw));
}
return false;
}
function findStyleValue(sourceCode, index) {
var node = sourceCode.getNodeByRangeIndex(index);
while (node && node.type !== 'Literal' && node.type !== 'TemplateLiteral') {
node = node.parent;
}
return node;
}
function isInSupportedStyleCall(context, value, importSources) {
var call = findContainingCall(value);
return Boolean(call && getStyleCalls(context).some(function (styleCall) {
return styleCall.node === call && importSources.includes(styleCall.importSource);
}));
}
/**
* Step up from `node` past any TypeScript wrappers (`as`, `satisfies`, `<T>`, `!`). Returns the
* outermost wrapper, which is what the next parent refers to, and that parent.
*/
function climbTypeWrappers(node) {
var current = node;
var parent = current.parent;
while (parent && isTypeWrapper(parent)) {
current = parent;
parent = current.parent;
}
return {
node: current,
parent: parent
};
}
/**
* Find the style call a value belongs to, looking through TypeScript wrappers at every level:
* `color: 'red !important' as const`, `'&:hover': {…} as const` and `css({…} as const)`.
*/
function findContainingCall(value) {
var _object;
var _climbTypeWrappers = climbTypeWrappers(value),
wrappedValue = _climbTypeWrappers.node,
property = _climbTypeWrappers.parent;
if ((property === null || property === void 0 ? void 0 : property.type) !== 'Property' || property.value !== wrappedValue) {
return null;
}
var object = property.parent;
if (((_object = object) === null || _object === void 0 ? void 0 : _object.type) !== 'ObjectExpression') {
return null;
}
while (object) {
var _parent$parent;
var _climbTypeWrappers2 = climbTypeWrappers(object),
wrappedObject = _climbTypeWrappers2.node,
parent = _climbTypeWrappers2.parent;
if ((parent === null || parent === void 0 ? void 0 : parent.type) === 'Property' && parent.value === wrappedObject) {
var _object2;
object = parent.parent;
if (((_object2 = object) === null || _object2 === void 0 ? void 0 : _object2.type) !== 'ObjectExpression') {
return null;
}
continue;
}
if ((parent === null || parent === void 0 ? void 0 : parent.type) === 'CallExpression' && parent.arguments.includes(wrappedObject)) {
return parent;
}
if ((parent === null || parent === void 0 ? void 0 : parent.type) === 'ArrowFunctionExpression' && parent.body === wrappedObject && ((_parent$parent = parent.parent) === null || _parent$parent === void 0 ? void 0 : _parent$parent.type) === 'CallExpression' && parent.parent.arguments.includes(parent)) {
return parent.parent;
}
return null;
}
return null;
}
function hasImportantSuffix(value) {
var index = value.length - 1;
while (index >= 0 && isWhitespace(value.charCodeAt(index))) {
index--;
}
var importantStart = index - 8;
if (importantStart < 0 || value.slice(importantStart, index + 1) !== 'important') {
return false;
}
index = importantStart - 1;
while (index >= 0 && isWhitespace(value.charCodeAt(index))) {
index--;
}
return value.charCodeAt(index) === 33;
}
function isWhitespace(character) {
return character === 32 || character >= 9 && character <= 13;
}