UNPKG

@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
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; }