@atlaskit/eslint-plugin-ui-styling-standard
Version:
The eslint plugin to enforce and educate on Atlassian's UI Styling Standard
152 lines (150 loc) • 5.2 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';
const 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
}
}
},
create(context, {
importSources
}) {
const sourceCode = getSourceCode(context);
if (!sourceCode.text.includes('important')) {
return {};
}
return {
Program() {
const {
text
} = sourceCode;
const reportedValues = new Set();
let importantIndex = text.indexOf('important');
while (importantIndex !== -1) {
const 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') {
const lastQuasi = node.quasis[node.quasis.length - 1];
return Boolean(lastQuasi && hasImportantSuffix(lastQuasi.value.raw));
}
return false;
}
function findStyleValue(sourceCode, index) {
let node = sourceCode.getNodeByRangeIndex(index);
while (node && node.type !== 'Literal' && node.type !== 'TemplateLiteral') {
node = node.parent;
}
return node;
}
function isInSupportedStyleCall(context, value, importSources) {
const call = findContainingCall(value);
return Boolean(call && getStyleCalls(context).some(styleCall => 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) {
let current = node;
let parent = current.parent;
while (parent && isTypeWrapper(parent)) {
current = parent;
parent = current.parent;
}
return {
node: current,
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;
const {
node: wrappedValue,
parent: property
} = climbTypeWrappers(value);
if ((property === null || property === void 0 ? void 0 : property.type) !== 'Property' || property.value !== wrappedValue) {
return null;
}
let object = property.parent;
if (((_object = object) === null || _object === void 0 ? void 0 : _object.type) !== 'ObjectExpression') {
return null;
}
while (object) {
var _parent$parent;
const {
node: wrappedObject,
parent
} = climbTypeWrappers(object);
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) {
let index = value.length - 1;
while (index >= 0 && isWhitespace(value.charCodeAt(index))) {
index--;
}
const 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;
}