UNPKG

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