UNPKG

@antebudimir/eslint-plugin-vanilla-extract

Version:

Comprehensive ESLint plugin for vanilla-extract with CSS property ordering, style validation, and best practices enforcement. Supports alphabetical, concentric and custom CSS ordering, auto-fixing, and zero-runtime safety.

166 lines (165 loc) 6.37 kB
import { TSESTree } from '@typescript-eslint/utils'; /** * Regex to match numbers with trailing zeros. * Matches patterns like: * - 1.0, 2.50, 0.0, 0.50 * - 1.0px, 2.50rem, 0.0em * - -1.0, -2.50px * * Groups: * 1: Optional minus sign * 2: Integer part * 3: Significant fractional digits (optional) * 4: Trailing zeros * 5: Optional unit */ const TRAILING_ZERO_REGEX = /^(-?)(\d+)\.(\d*[1-9])?(0+)([a-z%]+)?$/i; /** * Checks if a value has trailing zeros and returns the fixed value if needed. * * @param value The string value to check * @returns Object with hasTrailingZero flag and fixed value, or null if no trailing zeros */ export const checkTrailingZero = (value) => { const trimmedValue = value.trim(); const match = trimmedValue.match(TRAILING_ZERO_REGEX); if (!match) { return null; } const [, minus = '', integerPart, significantFractional = '', , unit = ''] = match; // Handle special case: 0.0 or 0.00 etc. should become just "0" if (integerPart === '0' && !significantFractional) { return { hasTrailingZero: true, fixed: '0', }; } // If there's no significant fractional part (e.g., "1.0" -> "1") if (!significantFractional) { return { hasTrailingZero: true, fixed: `${minus}${integerPart}${unit}`, }; } // If there's a significant fractional part (e.g., "1.50" -> "1.5") return { hasTrailingZero: true, fixed: `${minus}${integerPart}.${significantFractional}${unit}`, }; }; /** * Processes a single string value and checks for trailing zeros in all numeric values. * Handles strings with multiple numeric values (e.g., "1.0px 2.50em"). * Also handles values within function calls (e.g., "rotate(45.0deg)"). * * @param value The string value to process * @returns Object with hasTrailingZero flag and fixed value, or null if no trailing zeros */ export const processStringValue = (value) => { // First, try to match the entire value const directMatch = checkTrailingZero(value); if (directMatch?.hasTrailingZero) { return directMatch; } // Split by whitespace to handle multiple values const parts = value.split(/(\s+)/); let hasAnyTrailingZero = false; const fixedParts = parts.map((part) => { // Preserve whitespace if (/^\s+$/.test(part)) { return part; } // Try to match the whole part first const result = checkTrailingZero(part); if (result?.hasTrailingZero) { hasAnyTrailingZero = true; return result.fixed; } // If no match, try to find and replace numbers within the part (e.g., inside function calls) const regex = /(-?\d+)\.(\d*[1-9])?(0+)(?![0-9])([a-z%]+)?/gi; const fixedPart = part.replace(regex, (_, integerWithSign, significantFractional, __, unit) => { // Reconstruct the number without trailing zeros const integerPart = integerWithSign; const sig = significantFractional || ''; const u = unit || ''; // Handle 0.0 case - if it's zero and no unit, return just '0', otherwise keep the unit if (integerPart === '0' && !sig) { hasAnyTrailingZero = true; return u ? `0${u}` : '0'; } // Handle X.0 case if (!sig) { hasAnyTrailingZero = true; return `${integerPart}${u}`; } // Handle X.Y0 case hasAnyTrailingZero = true; return `${integerPart}.${sig}${u}`; }); return fixedPart; }); if (!hasAnyTrailingZero) { return null; } return { hasTrailingZero: true, fixed: fixedParts.join(''), }; }; /** * Recursively processes a style object, reporting and fixing instances of trailing zeros in numeric values. * * @param ruleContext The ESLint rule context. * @param node The ObjectExpression node representing the style object to be processed. */ export const processTrailingZeroInStyleObject = (ruleContext, node) => { node.properties.forEach((property) => { if (property.type !== 'Property') { return; } // Process direct string literal values if (property.value.type === 'Literal' && typeof property.value.value === 'string') { const result = processStringValue(property.value.value); if (result?.hasTrailingZero) { ruleContext.report({ node: property.value, messageId: 'trailingZero', data: { value: property.value.value, fixed: result.fixed, }, fix: (fixer) => fixer.replaceText(property.value, `'${result.fixed}'`), }); } } // Process numeric literal values (e.g., margin: 1.0) if (property.value.type === 'Literal' && typeof property.value.value === 'number') { // Use the raw property to get the original source text (which preserves trailing zeros) const rawValue = property.value.raw || property.value.value.toString(); const result = checkTrailingZero(rawValue); if (result?.hasTrailingZero) { ruleContext.report({ node: property.value, messageId: 'trailingZero', data: { value: rawValue, fixed: result.fixed, }, fix: (fixer) => fixer.replaceText(property.value, result.fixed), }); } } // Process nested objects (selectors, media queries, etc.) if (property.value.type === 'ObjectExpression') { processTrailingZeroInStyleObject(ruleContext, property.value); } // Process arrays (for styleVariants with array values) if (property.value.type === 'ArrayExpression') { property.value.elements.forEach((element) => { if (element && element.type === 'ObjectExpression') { processTrailingZeroInStyleObject(ruleContext, element); } }); } }); };