UNPKG

eslint-plugin-unicorn

Version:
146 lines (127 loc) • 4.41 kB
import {getVendorPrefix, shorthandToAffectedProperties} from './shared/css-shorthand-properties.js'; import {getStaticStringValue} from './ast/index.js'; import {unwrapTypeScriptExpression} from './utils/index.js'; const MESSAGE_ID = 'no-shorthand-property-overrides'; const messages = { [MESSAGE_ID]: 'The shorthand property `{{shorthand}}` overrides the previously declared `{{longhand}}` property.', }; const isStaticStyleValue = node => { node = unwrapTypeScriptExpression(node); const numericNode = node.type === 'UnaryExpression' && (node.operator === '+' || node.operator === '-') ? unwrapTypeScriptExpression(node.argument) : node; if (numericNode.type === 'Literal' && typeof numericNode.value === 'number') { return Number.isFinite(numericNode.value); } const value = getStaticStringValue(node); return typeof value === 'string' && value.trim() !== '' && !/!\s*important\b/iu.test(value); }; function * getJsxStyleProblems(attribute) { if ( attribute.name.type !== 'JSXIdentifier' || attribute.name.name !== 'style' || attribute.parent.name.type !== 'JSXIdentifier' || !/^[a-z]/u.test(attribute.parent.name.name) || attribute.value?.type !== 'JSXExpressionContainer' ) { return; } const object = unwrapTypeScriptExpression(attribute.value.expression); if ( object.type !== 'ObjectExpression' || object.properties.some(property => property.type !== 'Property' || property.computed || property.method || property.kind !== 'init') ) { return; } // Updating an existing object key keeps its original insertion order. const properties = new Map(object.properties.map(property => [property.key.name ?? property.key.value, property])); const declarations = new Map(); for (const [name, node] of properties) { if (typeof name !== 'string' || !isStaticStyleValue(node.value)) { continue; } let property = name.replaceAll(/[A-Z]/gu, letter => `-${letter.toLowerCase()}`); if (property.startsWith('ms-')) { property = `-${property}`; } const vendorPrefix = getVendorPrefix(property); const longhandProperties = shorthandToAffectedProperties.get(property.slice(vendorPrefix.length)); declarations.set(property, name); for (const longhand of longhandProperties ?? []) { const original = declarations.get(vendorPrefix + longhand); if (original) { yield { node: node.key, messageId: MESSAGE_ID, data: {shorthand: name, longhand: original}, }; } } } } /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { context.on('JSXAttribute', getJsxStyleProblems); context.on('Block', function * (block) { const declarations = new Map(); const importantDeclarations = new Set(); for (const declaration of block.children) { if (declaration.type !== 'Declaration') { continue; } const property = declaration.property.toLowerCase(); const vendorPrefix = getVendorPrefix(property); const unprefixedProperty = property.slice(vendorPrefix.length); const longhandProperties = shorthandToAffectedProperties.get(unprefixedProperty); declarations.set(property, declaration.property); // A later normal declaration does not beat an `!important` one, including longhands set by an important shorthand. if (declaration.important) { importantDeclarations.add(property); for (const longhand of longhandProperties ?? []) { importantDeclarations.add(vendorPrefix + longhand); } } if (!longhandProperties) { continue; } for (const longhand of longhandProperties) { const original = declarations.get(vendorPrefix + longhand); // An `!important` declaration beats any normal one, whatever the source order is, so a normal shorthand does not override an `!important` longhand if ( original && (declaration.important || !importantDeclarations.has(vendorPrefix + longhand)) ) { yield { node: declaration, messageId: MESSAGE_ID, data: { shorthand: declaration.property, longhand: original, }, }; } } } }); }; /** @type {import('eslint').Rule.RuleModule} */ const config = { create, meta: { type: 'problem', docs: { description: 'Disallow shorthand properties that override related longhand properties.', recommended: 'unopinionated', }, messages, languages: [ 'js/js', 'css/css', ], }, }; export default config;