diff --git a/CHANGELOG.md b/CHANGELOG.md index dce35d1..a965fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.12.0] - 2025-10-22 + +- Add new rule `no-trailing-zero` that flags and fixes unnecessary trailing zeros in numeric values +- Handles various CSS units, negative numbers, and decimal values +- Preserves non-trailing zeros in numbers like 11.01rem and 2.05em +- Includes comprehensive test coverage for edge cases + ## [1.11.1] - 2025-10-15 - Improve README structure and clarity diff --git a/README.md b/README.md index 4dee8b8..e08fd5a 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,7 @@ The recommended configuration enables the following rules with error severity: - `vanilla-extract/concentric-order`: Enforces concentric CSS property ordering - `vanilla-extract/no-empty-style-blocks`: Prevents empty style blocks +- `vanilla-extract/no-trailing-zero`: Disallows trailing zeros in numeric CSS values - `vanilla-extract/no-unknown-unit`: Prohibits usage of unrecognized CSS units - `vanilla-extract/no-zero-unit`: Removes unnecessary units for zero values @@ -464,6 +465,37 @@ export const recipeWithEmptyVariants = recipe({ }); ``` +## vanilla-extract/no-trailing-zero + +This rule disallows trailing zeros in numeric CSS values within vanilla-extract style objects. It helps maintain cleaner +and more consistent CSS by removing unnecessary trailing zeros from decimal numbers. + +```typescript +// ❌ Incorrect +import { style } from '@vanilla-extract/css'; + +export const myStyle = style({ + margin: '1.0px', + padding: '2.50rem', + opacity: 1.0, + lineHeight: 2.50, + width: '0.0em', + transition: 'all 0.30s ease', +}); + +// ✅ Correct +import { style } from '@vanilla-extract/css'; + +export const myStyle = style({ + margin: '1px', + padding: '2.5rem', + opacity: 1, + lineHeight: 2.5, + width: '0', + transition: 'all 0.3s ease', +}); +``` + ## vanilla-extract/no-unknown-unit This rule enforces the use of valid CSS units in vanilla-extract style objects. It prevents typos and non-standard units @@ -612,17 +644,17 @@ The roadmap outlines the project's current status and future plans: - Recommended ESLint configuration for the plugin. - `no-zero-unit` rule to disallow units when the value is zero. - `no-unknown-unit` rule to disallow unknown units. -- Support for using the plugin’s recommended config via the extends field (as discussed in +- `no-trailing-zero` rule to disallow trailing zeros in numbers. +- Support for using the plugin's recommended config via the extends field (as discussed in [issue #3](https://github.com/antebudimir/eslint-plugin-vanilla-extract/issues/3)) - Comprehensive rule testing. ### Current Work -- `no-number-trailing-zero` rule to disallow trailing zeros in numbers. +- `no-px-unit` rule to disallow use of `px` units with configurable whitelist. ### Upcoming Features -- `no-px-unit` rule to disallow use of `px` units with configurable whitelist. - `prefer-logical-properties` rule to enforce use of logical properties. - `prefer-theme-tokens` rule to enforce use of theme tokens instead of hard-coded values when available. - `no-global-style` rule to disallow use of `globalStyle` function. diff --git a/package.json b/package.json index c01e2ee..6b1ab07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@antebudimir/eslint-plugin-vanilla-extract", - "version": "1.11.1", + "version": "1.12.0", "description": "ESLint plugin for enforcing best practices in vanilla-extract CSS styles, including CSS property ordering and additional linting rules.", "author": "Ante Budimir", "license": "MIT", diff --git a/src/css-rules/no-trailing-zero/_tests_/no-trailing-zero.test.ts b/src/css-rules/no-trailing-zero/_tests_/no-trailing-zero.test.ts new file mode 100644 index 0000000..2b7c188 --- /dev/null +++ b/src/css-rules/no-trailing-zero/_tests_/no-trailing-zero.test.ts @@ -0,0 +1,538 @@ +import tsParser from '@typescript-eslint/parser'; +import { run } from 'eslint-vitest-rule-tester'; +import noTrailingZeroRule from '../rule-definition.js'; + +run({ + name: 'vanilla-extract/no-trailing-zero', + rule: noTrailingZeroRule, + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + }, + valid: [ + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '0', + padding: '1px', + width: '1.5rem', + height: '0.5em', + fontSize: '2rem', + }); + `, + name: 'should allow values without trailing zeros', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: 0, + padding: 1, + opacity: 0.5, + lineHeight: 1.5, + }); + `, + name: 'should allow numeric literals without trailing zeros', + }, + { + code: ` + import { recipe } from '@vanilla-extract/css'; + const myRecipe = recipe({ + base: { + margin: '1px', + padding: '0.5rem', + }, + variants: { + size: { + small: { + height: '10px', + width: '0.75em', + }, + }, + }, + }); + `, + name: 'should allow recipe values without trailing zeros', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + ...spreadProps, + margin: '1px', + '@media': { + '1.0rem': '0.5rem' // Key shouldn't be checked + } + }); + `, + name: 'should ignore spread elements and object keys', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: \`1.0\${someUnit}\`, // Template literal + padding: someVariable, + width: calculateWidth(), + }); + `, + name: 'should ignore non-literal values', + }, + { + code: ` + import { globalStyle } from '@vanilla-extract/css'; + const callExpression = someObject.fontFace({ src: '...' }); // Non-Identifier callee + `, + name: 'should ignore member expression callees', + }, + { + code: ` + import { fontFace } from '@vanilla-extract/css'; + fontFace(); // Missing arguments + `, + name: 'should handle missing fontFace arguments', + }, + { + code: ` + import { globalFontFace } from '@vanilla-extract/css'; + globalFontFace('my-font'); // Missing style argument + `, + name: 'should handle missing globalFontFace style argument', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '10px', + padding: '100rem', + width: '1000%', + }); + `, + name: 'should allow integers without decimal points', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '11.01rem', + padding: '2.05em', + width: '0.101%', + height: '10.001px', + }); + `, + name: 'should not flag zeros in the middle of decimal numbers', + }, + ], + invalid: [ + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '1.0px', + padding: '2.50rem', + }); + `, + errors: [{ messageId: 'trailingZero' }, { messageId: 'trailingZero' }], + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '1px', + padding: '2.5rem', + }); + `, + name: 'should fix trailing zeros in string values with units', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + opacity: 1.0, + lineHeight: 2.50, + }); + `, + errors: [{ messageId: 'trailingZero' }, { messageId: 'trailingZero' }], + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + opacity: 1, + lineHeight: 2.5, + }); + `, + name: 'should fix trailing zeros in numeric literals', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '0.0', + padding: '0.00px', + width: '0.0rem', + }); + `, + errors: [{ messageId: 'trailingZero' }, { messageId: 'trailingZero' }, { messageId: 'trailingZero' }], + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '0', + padding: '0', + width: '0', + }); + `, + name: 'should convert 0.0 to 0', + }, + { + code: ` + import { recipe } from '@vanilla-extract/css'; + const myRecipe = recipe({ + base: { + margin: '1.0px', + }, + variants: { + size: { + small: { + height: '2.50vh', + }, + }, + }, + }); + `, + errors: [{ messageId: 'trailingZero' }, { messageId: 'trailingZero' }], + output: ` + import { recipe } from '@vanilla-extract/css'; + const myRecipe = recipe({ + base: { + margin: '1px', + }, + variants: { + size: { + small: { + height: '2.5vh', + }, + }, + }, + }); + `, + name: 'should handle recipe trailing zeros', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '1.0px', + '@media': { + '(min-width: 768px)': { + padding: '2.50rem' + } + } + }); + `, + errors: 2, + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '1px', + '@media': { + '(min-width: 768px)': { + padding: '2.5rem' + } + } + }); + `, + name: 'should handle nested media queries', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + '::before': { + content: '""', + margin: '1.0px' + } + }); + `, + errors: 1, + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + '::before': { + content: '""', + margin: '1px' + } + }); + `, + name: 'should handle pseudo-elements', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '1.0px', + nested: { + object: { + padding: '2.50rem', + deeper: { + width: '3.00%' + } + } + } + }); + `, + errors: 3, + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '1px', + nested: { + object: { + padding: '2.5rem', + deeper: { + width: '3%' + } + } + } + }); + `, + name: 'should handle multiple levels of nesting', + }, + { + code: ` + import { fontFace, globalFontFace } from '@vanilla-extract/css'; + + fontFace({ + src: '...', + lineGap: '1.0rem' + }); + + globalFontFace('my-font', { + src: '...', + sizeAdjust: '100.0%' + }); + `, + errors: 2, + output: ` + import { fontFace, globalFontFace } from '@vanilla-extract/css'; + + fontFace({ + src: '...', + lineGap: '1rem' + }); + + globalFontFace('my-font', { + src: '...', + sizeAdjust: '100%' + }); + `, + name: 'should handle fontFace and globalFontFace arguments', + }, + { + code: ` + import { globalKeyframes, globalStyle } from '@vanilla-extract/css'; + + globalKeyframes('spin', { + '0%': { transform: 'rotate(0deg)' }, + '100%': { transform: 'rotate(360.0deg)' } + }); + + globalStyle('html', { + margin: '1.0px', + padding: '2.50rem' + }); + `, + errors: 3, + output: ` + import { globalKeyframes, globalStyle } from '@vanilla-extract/css'; + + globalKeyframes('spin', { + '0%': { transform: 'rotate(0deg)' }, + '100%': { transform: 'rotate(360deg)' } + }); + + globalStyle('html', { + margin: '1px', + padding: '2.5rem' + }); + `, + name: 'should handle globalKeyframes and globalStyle arguments', + }, + { + code: ` + import { globalStyle } from '@vanilla-extract/css'; + globalStyle('html', { + '@media': { + '(min-width: 768px)': { + margin: '1.0px' + } + } + }); + `, + errors: 1, + output: ` + import { globalStyle } from '@vanilla-extract/css'; + globalStyle('html', { + '@media': { + '(min-width: 768px)': { + margin: '1px' + } + } + }); + `, + name: 'should handle nested globalStyle arguments', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '-1.0px', + padding: '-2.50rem', + top: '-0.0vh', + }); + `, + errors: [{ messageId: 'trailingZero' }, { messageId: 'trailingZero' }, { messageId: 'trailingZero' }], + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '-1px', + padding: '-2.5rem', + top: '0', + }); + `, + name: 'should handle negative values with trailing zeros', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '1.50em', + padding: '0.50rem', + width: '10.00%', + }); + `, + errors: [{ messageId: 'trailingZero' }, { messageId: 'trailingZero' }, { messageId: 'trailingZero' }], + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '1.5em', + padding: '0.5rem', + width: '10%', + }); + `, + name: 'should remove trailing zeros from decimal values', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + padding: '1.0px 2.50rem 3.00em 0.50vh', + }); + `, + errors: 1, + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + padding: '1px 2.5rem 3em 0.5vh', + }); + `, + name: 'should handle multiple values in a single string', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + transition: 'all 0.30s ease', + animation: 'spin 2.0s linear', + }); + `, + errors: 2, + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + transition: 'all 0.3s ease', + animation: 'spin 2s linear', + }); + `, + name: 'should handle time units', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + transform: 'rotate(45.0deg)', + filter: 'hue-rotate(180.00deg)', + }); + `, + errors: 2, + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + transform: 'rotate(45deg)', + filter: 'hue-rotate(180deg)', + }); + `, + name: 'should handle angle units', + }, + { + code: ` + import { styleVariants } from '@vanilla-extract/css'; + const variants = styleVariants({ + small: { padding: '1.0px' }, + medium: { padding: '2.50px' }, + large: { padding: '3.00px' }, + }); + `, + errors: 3, + output: ` + import { styleVariants } from '@vanilla-extract/css'; + const variants = styleVariants({ + small: { padding: '1px' }, + medium: { padding: '2.5px' }, + large: { padding: '3px' }, + }); + `, + name: 'should handle styleVariants', + }, + { + code: ` + import { keyframes } from '@vanilla-extract/css'; + const spin = keyframes({ + '0%': { transform: 'rotate(0.0deg)' }, + '50%': { transform: 'rotate(180.0deg)' }, + '100%': { transform: 'rotate(360.0deg)' }, + }); + `, + errors: 3, + output: ` + import { keyframes } from '@vanilla-extract/css'; + const spin = keyframes({ + '0%': { transform: 'rotate(0deg)' }, + '50%': { transform: 'rotate(180deg)' }, + '100%': { transform: 'rotate(360deg)' }, + }); + `, + name: 'should handle keyframes', + }, + { + code: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '1.000px', + padding: '2.5000rem', + width: '0.00000em', + }); + `, + errors: 3, + output: ` + import { style } from '@vanilla-extract/css'; + const myStyle = style({ + margin: '1px', + padding: '2.5rem', + width: '0', + }); + `, + name: 'should handle multiple trailing zeros', + }, + ], +}); diff --git a/src/css-rules/no-trailing-zero/index.ts b/src/css-rules/no-trailing-zero/index.ts new file mode 100644 index 0000000..58ee791 --- /dev/null +++ b/src/css-rules/no-trailing-zero/index.ts @@ -0,0 +1 @@ +export { default } from './rule-definition.js'; diff --git a/src/css-rules/no-trailing-zero/rule-definition.ts b/src/css-rules/no-trailing-zero/rule-definition.ts new file mode 100644 index 0000000..7e8be28 --- /dev/null +++ b/src/css-rules/no-trailing-zero/rule-definition.ts @@ -0,0 +1,23 @@ +import type { Rule } from 'eslint'; +import { createTrailingZeroVisitors } from './trailing-zero-visitor-creator.js'; + +const noTrailingZeroRule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow trailing zeros in numeric CSS values', + category: 'Stylistic Issues', + recommended: true, + }, + fixable: 'code', + schema: [], + messages: { + trailingZero: 'Numeric value "{{ value }}" has unnecessary trailing zeros. Use "{{ fixed }}" instead.', + }, + }, + create(context) { + return createTrailingZeroVisitors(context); + }, +}; + +export default noTrailingZeroRule; diff --git a/src/css-rules/no-trailing-zero/trailing-zero-processor.ts b/src/css-rules/no-trailing-zero/trailing-zero-processor.ts new file mode 100644 index 0000000..3560891 --- /dev/null +++ b/src/css-rules/no-trailing-zero/trailing-zero-processor.ts @@ -0,0 +1,197 @@ +import type { Rule } from 'eslint'; +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: string): { hasTrailingZero: boolean; fixed: string } | null => { + 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: string): { hasTrailingZero: boolean; fixed: string } | null => { + // 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, + (_: string, integerWithSign: string, significantFractional: string, __: string, unit: string) => { + // 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: Rule.RuleContext, + node: TSESTree.ObjectExpression, +): void => { + 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); + } + }); + } + }); +}; diff --git a/src/css-rules/no-trailing-zero/trailing-zero-visitor-creator.ts b/src/css-rules/no-trailing-zero/trailing-zero-visitor-creator.ts new file mode 100644 index 0000000..dbd9f4e --- /dev/null +++ b/src/css-rules/no-trailing-zero/trailing-zero-visitor-creator.ts @@ -0,0 +1,103 @@ +import type { Rule } from 'eslint'; +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import { processRecipeProperties } from '../shared-utils/recipe-property-processor.js'; +import { ReferenceTracker, createReferenceTrackingVisitor } from '../shared-utils/reference-tracker.js'; +import { processStyleNode } from '../shared-utils/style-node-processor.js'; +import { processTrailingZeroInStyleObject } from './trailing-zero-processor.js'; + +/** + * Creates ESLint rule visitors for detecting and processing trailing zeros in numeric CSS values. + * Uses reference tracking to automatically detect vanilla-extract functions based on their import statements. + * + * @param context The ESLint rule context. + * @returns An object with visitor functions for the ESLint rule. + */ +export const createTrailingZeroVisitors = (context: Rule.RuleContext): Rule.RuleListener => { + const tracker = new ReferenceTracker(); + const trackingVisitor = createReferenceTrackingVisitor(tracker); + + return { + // Include the reference tracking visitors + ...trackingVisitor, + + CallExpression(node) { + if (node.callee.type !== AST_NODE_TYPES.Identifier) { + return; + } + + const functionName = node.callee.name; + + // Check if this function is tracked as a vanilla-extract function + if (!tracker.isTrackedFunction(functionName)) { + return; + } + + const originalName = tracker.getOriginalName(functionName); + if (!originalName) { + return; + } + + // Handle different function types based on their original imported name + switch (originalName) { + case 'fontFace': + if (node.arguments.length > 0 && node.arguments[0]?.type === AST_NODE_TYPES.ObjectExpression) { + processTrailingZeroInStyleObject(context, node.arguments[0] as TSESTree.ObjectExpression); + } + break; + + case 'globalFontFace': + if (node.arguments.length > 1 && node.arguments[1]?.type === AST_NODE_TYPES.ObjectExpression) { + processTrailingZeroInStyleObject(context, node.arguments[1] as TSESTree.ObjectExpression); + } + break; + + case 'style': + if (node.arguments.length > 0) { + processStyleNode(context, node.arguments[0] as TSESTree.ObjectExpression, processTrailingZeroInStyleObject); + } + break; + + case 'styleVariants': + case 'keyframes': + // For styleVariants and keyframes, the argument is an object where each property value is a style object + if (node.arguments.length > 0 && node.arguments[0]?.type === AST_NODE_TYPES.ObjectExpression) { + const variantsObject = node.arguments[0] as TSESTree.ObjectExpression; + variantsObject.properties.forEach((property) => { + if (property.type === 'Property' && property.value.type === 'ObjectExpression') { + processTrailingZeroInStyleObject(context, property.value); + } + }); + } + break; + + case 'globalStyle': + if (node.arguments.length >= 2) { + processStyleNode(context, node.arguments[1] as TSESTree.ObjectExpression, processTrailingZeroInStyleObject); + } + break; + + case 'globalKeyframes': + // For globalKeyframes, the second argument is an object where each property value is a style object + if (node.arguments.length >= 2 && node.arguments[1]?.type === AST_NODE_TYPES.ObjectExpression) { + const keyframesObject = node.arguments[1] as TSESTree.ObjectExpression; + keyframesObject.properties.forEach((property) => { + if (property.type === 'Property' && property.value.type === 'ObjectExpression') { + processTrailingZeroInStyleObject(context, property.value); + } + }); + } + break; + + case 'recipe': + if (node.arguments.length > 0 && node.arguments[0]?.type === AST_NODE_TYPES.ObjectExpression) { + processRecipeProperties( + context, + node.arguments[0] as TSESTree.ObjectExpression, + processTrailingZeroInStyleObject, + ); + } + break; + } + }, + }; +}; diff --git a/src/index.ts b/src/index.ts index aff2672..c0e9d04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,19 +2,21 @@ import alphabeticalOrderRule from './css-rules/alphabetical-order/index.js'; import concentricOrderRule from './css-rules/concentric-order/index.js'; import customOrderRule from './css-rules/custom-order/rule-definition.js'; import noEmptyStyleBlocksRule from './css-rules/no-empty-blocks/rule-definition.js'; +import noTrailingZeroRule from './css-rules/no-trailing-zero/rule-definition.js'; import noUnknownUnitRule from './css-rules/no-unknown-unit/rule-definition.js'; import noZeroUnitRule from './css-rules/no-zero-unit/rule-definition.js'; const vanillaExtract = { meta: { name: '@antebudimir/eslint-plugin-vanilla-extract', - version: '1.11.1', + version: '1.12.0', }, rules: { 'alphabetical-order': alphabeticalOrderRule, 'concentric-order': concentricOrderRule, 'custom-order': customOrderRule, 'no-empty-style-blocks': noEmptyStyleBlocksRule, + 'no-trailing-zero': noTrailingZeroRule, 'no-unknown-unit': noUnknownUnitRule, 'no-zero-unit': noZeroUnitRule, }, @@ -29,6 +31,7 @@ Object.assign(vanillaExtract.configs, { rules: { 'vanilla-extract/concentric-order': 'error', 'vanilla-extract/no-empty-style-blocks': 'error', + 'vanilla-extract/no-trailing-zero': 'error', 'vanilla-extract/no-unknown-unit': 'error', 'vanilla-extract/no-zero-unit': 'error', },