diff --git a/CHANGELOG.md b/CHANGELOG.md index 6550b53..eb774b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,9 @@ 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.16.0] - 2025-12-01 - -- Add new rule `no-unitless-values` that disallows unitless numeric values for CSS properties that require units ([issue #6](https://github.com/antebudimir/eslint-plugin-vanilla-extract/issues/6)) - - Flags both numeric literals (e.g., `width: 100`) and string literals with unitless numbers (e.g., `width: '100'`) - - Allows zero values without units (valid CSS) and properties that accept unitless values (opacity, zIndex, lineHeight, etc.) - - Configurable allowlist via `allow` option to exclude specific properties from checking - - Optional rule (not included in recommended config) - teams can enable when they prefer explicit units over vanilla-extract's automatic px conversion - ## [1.15.1] - 2025-11-22 -- Fix [issue #7](https://github.com/antebudimir/eslint-plugin-vanilla-extract/issues/7) to prevent false positives for `sprinkles()`/`style()`/`recipe()` calls with non-empty object arguments while continuing to flag bare `({})` calls +- Fix [issue #7](https://github.com/antebudimir/eslint-plugin-vanilla-extract/issues/3) to prevent false positives for `sprinkles()`/`style()`/`recipe()` calls with non-empty object arguments while continuing to flag bare `({})` calls - Add regression tests covering empty and non-empty call expressions in recipe base/variants to guard against future regressions ## [1.15.0] - 2025-11-14 diff --git a/README.md b/README.md index 185724f..8e1ff4c 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,6 @@ The recommended configuration enables the following rules with error severity: - `vanilla-extract/alphabetical-order`: Alternative ordering rule (alphabetical sorting) - `vanilla-extract/custom-order`: Alternative ordering rule (custom group-based sorting) - `vanilla-extract/no-px-unit`: Disallows px units with an optional allowlist -- `vanilla-extract/no-unitless-values`: Disallows unitless numeric values for CSS properties that require units - `vanilla-extract/prefer-logical-properties`: Enforces logical CSS properties over physical directional properties - `vanilla-extract/prefer-theme-tokens`: Enforces theme tokens instead of hard-coded values for colors, spacing, font sizes, border radius, border widths, shadows, z-index, opacity, font weights, and transitions/animations (optionally evaluates helper functions and template literals) @@ -537,77 +536,6 @@ export const myStyle = style({ }); ``` -### vanilla-extract/no-unitless-values - -This rule disallows unitless numeric values for CSS properties that require units in vanilla-extract style objects. It helps teams that prefer explicit units avoid confusion, as vanilla-extract automatically converts unitless numbers to `px` at runtime. - -**Note:** This is an optional rule (not enabled in recommended config). Enable it only if your team prefers explicit units over vanilla-extract's automatic `px` conversion. - -Configuration with allowed properties: - -```json -{ - "rules": { - "vanilla-extract/no-unitless-values": ["warn", { "allow": ["width", "height"] }] - } -} -``` - -```typescript -// ❌ Incorrect -import { style } from '@vanilla-extract/css'; - -export const myStyle = style({ - width: 100, - margin: 20, - padding: 10.5, - height: '50', - top: '-10', -}); - -// ✅ Correct -import { style } from '@vanilla-extract/css'; - -export const myStyle = style({ - width: '100px', - margin: '20px', - padding: 0, - height: '50rem', - opacity: 0.5, // opacity accepts unitless values - lineHeight: 1.5, // line-height accepts unitless values - zIndex: 10, // z-index accepts unitless values -}); -``` - -**Properties that require units:** - -- **Box model:** `width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `min-width`, `max-width`, `min-height`, `max-height` -- **Spacing:** `margin`, `marginTop`, `marginRight`, `marginBottom`, `marginLeft`, `marginBlock`, `marginBlockStart`, `marginBlockEnd`, `marginInline`, `marginInlineStart`, `marginInlineEnd`, `margin-top`, `margin-right`, `margin-bottom`, `margin-left`, `margin-block`, `margin-block-start`, `margin-block-end`, `margin-inline`, `margin-inline-start`, `margin-inline-end`, `padding`, `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft`, `paddingBlock`, `paddingBlockStart`, `paddingBlockEnd`, `paddingInline`, `paddingInlineStart`, `paddingInlineEnd`, `padding-top`, `padding-right`, `padding-bottom`, `padding-left`, `padding-block`, `padding-block-start`, `padding-block-end`, `padding-inline`, `padding-inline-start`, `padding-inline-end` -- **Positioning:** `top`, `right`, `bottom`, `left`, `inset`, `insetBlock`, `insetBlockStart`, `insetBlockEnd`, `insetInline`, `insetInlineStart`, `insetInlineEnd`, `inset-block`, `inset-block-start`, `inset-block-end`, `inset-inline`, `inset-inline-start`, `inset-inline-end` -- **Border:** `borderWidth`, `borderTopWidth`, `borderRightWidth`, `borderBottomWidth`, `borderLeftWidth`, `borderBlockWidth`, `borderBlockStartWidth`, `borderBlockEndWidth`, `borderInlineWidth`, `borderInlineStartWidth`, `borderInlineEndWidth`, `border-width`, `border-top-width`, `border-right-width`, `border-bottom-width`, `border-left-width`, `border-block-width`, `border-block-start-width`, `border-block-end-width`, `border-inline-width`, `border-inline-start-width`, `border-inline-end-width`, `borderRadius`, `borderTopLeftRadius`, `borderTopRightRadius`, `borderBottomLeftRadius`, `borderBottomRightRadius`, `borderStartStartRadius`, `borderStartEndRadius`, `borderEndStartRadius`, `borderEndEndRadius`, `border-radius`, `border-top-left-radius`, `border-top-right-radius`, `border-bottom-left-radius`, `border-bottom-right-radius`, `border-start-start-radius`, `border-start-end-radius`, `border-end-start-radius`, `border-end-end-radius` -- **Typography:** `fontSize`, `font-size`, `letterSpacing`, `letter-spacing`, `wordSpacing`, `word-spacing`, `textIndent`, `text-indent` -- **Layout:** `gap`, `rowGap`, `columnGap`, `row-gap`, `column-gap`, `flexBasis`, `flex-basis` -- **Outline:** `outlineWidth`, `outline-width`, `outlineOffset`, `outline-offset` -- **Other:** `blockSize`, `inlineSize`, `minBlockSize`, `maxBlockSize`, `minInlineSize`, `maxInlineSize`, `block-size`, `inline-size`, `min-block-size`, `max-block-size`, `min-inline-size`, `max-inline-size` - -**Properties that accept unitless values:** - -- **Common:** `opacity`, `zIndex`, `z-index`, `lineHeight`, `line-height`, `flexGrow`, `flex-grow`, `flexShrink`, `flex-shrink`, `order`, `fontWeight`, `font-weight`, `zoom` -- **Animation:** `animationIterationCount`, `animation-iteration-count` -- **Layout:** `columnCount`, `column-count`, `orphans`, `widows` -- **Grid:** `gridColumn`, `grid-column`, `gridColumnEnd`, `grid-column-end`, `gridColumnStart`, `grid-column-start`, `gridRow`, `grid-row`, `gridRowEnd`, `grid-row-end`, `gridRowStart`, `grid-row-start` -- **SVG:** `fillOpacity`, `fill-opacity`, `strokeOpacity`, `stroke-opacity`, `strokeMiterlimit`, `stroke-miterlimit` - -**Why use this rule?** - -While vanilla-extract safely converts unitless numbers to `px`, some teams prefer explicit units because: - -1. It makes the intended unit clear (px, rem, em, %, etc.) -2. It prevents accidental use of px when rem or other units are preferred -3. It aligns with CSS best practices of being explicit about units - -**Auto-fix:** Not available. Since different teams prefer different units (px, rem, em, %), you must manually add your preferred unit. - ### 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 @@ -907,7 +835,10 @@ The roadmap outlines the project's current status and future plans: - `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 theme tokens instead of hard-coded values for colors, spacing, font sizes, border radius, border widths, shadows, z-index, opacity, font weights, and transitions/animations (optionally evaluates helper functions and template literals). -- `no-unitless-values` rule to disallow unitless numeric values for CSS properties that require units. + +### Current Work + +- `no-unitless-values` rule that disallows numeric literals for CSS properties that are not unitless in CSS. ### Upcoming Features diff --git a/package.json b/package.json index b8fbded..648e93a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@antebudimir/eslint-plugin-vanilla-extract", - "version": "1.16.0", + "version": "1.15.1", "description": "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.", "author": "Ante Budimir", "license": "MIT", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f382a0..ca799dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,9 +39,6 @@ importers: '@vanilla-extract/recipes': specifier: ^0.5.5 version: 0.5.5(@vanilla-extract/css@1.17.1) - '@vanilla-extract/sprinkles': - specifier: ^1.6.0 - version: 1.6.5(@vanilla-extract/css@1.17.1) '@vitest/coverage-v8': specifier: 3.0.8 version: 3.0.8(vitest@3.0.8(@types/node@20.17.24)(tsx@4.19.3)) @@ -614,11 +611,6 @@ packages: peerDependencies: '@vanilla-extract/css': ^1.0.0 - '@vanilla-extract/sprinkles@1.6.5': - resolution: {integrity: sha512-HOYidLONR/SeGk8NBAeI64I4gYdsMX9vJmniL13ZcLVwawyK0s2GUENEAcGA+GYLIoeyQB61UqmhqPodJry7zA==} - peerDependencies: - '@vanilla-extract/css': ^1.0.0 - '@vitest/coverage-v8@3.0.8': resolution: {integrity: sha512-y7SAKsQirsEJ2F8bulBck4DoluhI2EEgTimHd6EEUgJBGKy9tC25cpywh1MH4FvDGoG2Unt7+asVd1kj4qOSAw==} peerDependencies: @@ -2326,10 +2318,6 @@ snapshots: dependencies: '@vanilla-extract/css': 1.17.1 - '@vanilla-extract/sprinkles@1.6.5(@vanilla-extract/css@1.17.1)': - dependencies: - '@vanilla-extract/css': 1.17.1 - '@vitest/coverage-v8@3.0.8(vitest@3.0.8(@types/node@20.17.24)(tsx@4.19.3))': dependencies: '@ampproject/remapping': 2.3.0 diff --git a/src/css-rules/no-trailing-zero/index.ts b/src/css-rules/no-trailing-zero/index.ts index 25d6e11..58ee791 100644 --- a/src/css-rules/no-trailing-zero/index.ts +++ b/src/css-rules/no-trailing-zero/index.ts @@ -1,3 +1 @@ -import noTrailingZeroRule from './rule-definition.js'; - -export default noTrailingZeroRule; +export { default } from './rule-definition.js'; diff --git a/src/css-rules/no-unitless-values/_tests_/no-unitless-values.test.ts b/src/css-rules/no-unitless-values/_tests_/no-unitless-values.test.ts deleted file mode 100644 index 2f45564..0000000 --- a/src/css-rules/no-unitless-values/_tests_/no-unitless-values.test.ts +++ /dev/null @@ -1,472 +0,0 @@ -import tsParser from '@typescript-eslint/parser'; -import { run } from 'eslint-vitest-rule-tester'; -import noUnitlessValuesRule from '../rule-definition.js'; - -run({ - name: 'vanilla-extract/no-unitless-values', - rule: noUnitlessValuesRule, - languageOptions: { - parser: tsParser, - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - }, - }, - valid: [ - // Zero values are allowed - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - margin: 0, - padding: 0, - width: 0, - }); - `, - name: 'should allow zero values without units', - }, - - // String values with units - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - width: '100px', - margin: '20px', - padding: '1rem', - fontSize: '16px', - }); - `, - name: 'should allow string values with units', - }, - - // String zero values - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - margin: '0', - padding: '0', - }); - `, - name: 'should allow string zero values without units', - }, - - // Unitless-valid properties - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - opacity: 0.5, - zIndex: 10, - lineHeight: 1.5, - flexGrow: 1, - flexShrink: 0, - order: 2, - fontWeight: 700, - zoom: 1.2, - }); - `, - name: 'should allow unitless values for properties that accept them', - }, - - // Recipe with valid values - { - code: ` - import { recipe } from '@vanilla-extract/css'; - const myRecipe = recipe({ - base: { - margin: '0', - padding: 0, - opacity: 0.8, - }, - variants: { - size: { - small: { - height: '10px', - width: '10px', - zIndex: 1, - }, - }, - }, - }); - `, - name: 'should allow valid values in recipe', - }, - - // Template literals (not checked) - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - margin: \`\${someValue}px\`, - padding: someVariable, - }); - `, - name: 'should ignore template literals and variables', - }, - - // Nested selectors with valid values - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - width: '100px', - ':hover': { - margin: '20px', - opacity: 0.8, - }, - }); - `, - name: 'should allow valid values in nested selectors', - }, - - // Media queries with valid values - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - margin: 0, - '@media': { - '(min-width: 768px)': { - padding: '20px', - zIndex: 10, - } - } - }); - `, - name: 'should allow valid values in media queries', - }, - - // globalStyle with valid values - { - code: ` - import { globalStyle } from '@vanilla-extract/css'; - globalStyle('html', { - margin: '0', - padding: 0, - lineHeight: 1.5, - }); - `, - name: 'should allow valid values in globalStyle', - }, - - // fontFace with valid values - { - code: ` - import { fontFace } from '@vanilla-extract/css'; - fontFace({ - src: 'url(...)', - fontWeight: 400, - }); - `, - name: 'should allow valid values in fontFace', - }, - - // keyframes (animation values are strings) - { - code: ` - import { keyframes } from '@vanilla-extract/css'; - const spin = keyframes({ - '0%': { transform: 'rotate(0deg)' }, - '100%': { transform: 'rotate(360deg)' } - }); - `, - name: 'should allow keyframes with string values', - }, - - // allow option - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - width: 100, - margin: '20px', - }); - `, - options: [{ allow: ['width'] }], - name: 'should allow properties specified in allow option', - }, - - // Kebab-case unitless-valid properties - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - 'z-index': 10, - 'line-height': 1.5, - 'flex-grow': 1, - }); - `, - name: 'should allow unitless values for kebab-case unitless-valid properties', - }, - ], - - invalid: [ - // Basic unitless values - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - width: 100, - margin: 20, - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'width', value: '100' } }, - { messageId: 'noUnitlessValue', data: { property: 'margin', value: '20' } }, - ], - name: 'should flag unitless numeric values for length properties', - }, - - // Decimal values - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - padding: 10.5, - fontSize: 16.5, - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'padding', value: '10.5' } }, - { messageId: 'noUnitlessValue', data: { property: 'fontSize', value: '16.5' } }, - ], - name: 'should flag decimal unitless values', - }, - - // Recipe with unitless values - { - code: ` - import { recipe } from '@vanilla-extract/css'; - const myRecipe = recipe({ - base: { - margin: 10, - }, - variants: { - size: { - small: { - height: 20, - }, - }, - }, - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'margin', value: '10' } }, - { messageId: 'noUnitlessValue', data: { property: 'height', value: '20' } }, - ], - name: 'should flag unitless values in recipe', - }, - - // Nested selectors with unitless values - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - width: 100, - ':hover': { - margin: 20, - }, - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'width', value: '100' } }, - { messageId: 'noUnitlessValue', data: { property: 'margin', value: '20' } }, - ], - name: 'should flag unitless values in nested selectors', - }, - - // Media queries with unitless values - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - '@media': { - '(min-width: 768px)': { - padding: 20, - } - } - }); - `, - errors: [{ messageId: 'noUnitlessValue', data: { property: 'padding', value: '20' } }], - name: 'should flag unitless values in media queries', - }, - - // Multiple levels of nesting - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - margin: 10, - nested: { - object: { - padding: 20, - deeper: { - width: 30 - } - } - } - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'margin', value: '10' } }, - { messageId: 'noUnitlessValue', data: { property: 'padding', value: '20' } }, - { messageId: 'noUnitlessValue', data: { property: 'width', value: '30' } }, - ], - name: 'should flag unitless values in deeply nested objects', - }, - - // globalStyle with unitless values - { - code: ` - import { globalStyle } from '@vanilla-extract/css'; - globalStyle('html', { - margin: 10, - padding: 20, - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'margin', value: '10' } }, - { messageId: 'noUnitlessValue', data: { property: 'padding', value: '20' } }, - ], - name: 'should flag unitless values in globalStyle', - }, - - // Various length properties - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - width: 100, - height: 200, - minWidth: 50, - maxWidth: 500, - top: 10, - left: 20, - borderWidth: 2, - borderRadius: 5, - gap: 15, - fontSize: 16, - }); - `, - errors: 10, - name: 'should flag all unitless values for various length properties', - }, - - // Kebab-case properties - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - 'margin-top': 10, - 'padding-left': 20, - 'font-size': 16, - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'margin-top', value: '10' } }, - { messageId: 'noUnitlessValue', data: { property: 'padding-left', value: '20' } }, - { messageId: 'noUnitlessValue', data: { property: 'font-size', value: '16' } }, - ], - name: 'should flag unitless values for kebab-case properties', - }, - - // Logical properties - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - marginBlock: 10, - paddingInline: 20, - insetBlockStart: 5, - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'marginBlock', value: '10' } }, - { messageId: 'noUnitlessValue', data: { property: 'paddingInline', value: '20' } }, - { messageId: 'noUnitlessValue', data: { property: 'insetBlockStart', value: '5' } }, - ], - name: 'should flag unitless values for logical properties', - }, - - // styleVariants - { - code: ` - import { styleVariants } from '@vanilla-extract/css'; - const variants = styleVariants({ - small: { width: 10 }, - large: { width: 100 }, - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'width', value: '10' } }, - { messageId: 'noUnitlessValue', data: { property: 'width', value: '100' } }, - ], - name: 'should flag unitless values in styleVariants', - }, - - // globalKeyframes - { - code: ` - import { globalKeyframes } from '@vanilla-extract/css'; - globalKeyframes('slide', { - '0%': { left: 0 }, - '100%': { left: 100 } - }); - `, - errors: [{ messageId: 'noUnitlessValue', data: { property: 'left', value: '100' } }], - name: 'should flag unitless values in globalKeyframes', - }, - - // Negative values - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - margin: -10, - top: -5, - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'margin', value: '-10' } }, - { messageId: 'noUnitlessValue', data: { property: 'top', value: '-5' } }, - ], - name: 'should flag negative unitless values', - }, - - // String unitless values - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - width: '100', - margin: '20', - padding: '10.5', - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'width', value: '100' } }, - { messageId: 'noUnitlessValue', data: { property: 'margin', value: '20' } }, - { messageId: 'noUnitlessValue', data: { property: 'padding', value: '10.5' } }, - ], - name: 'should flag string unitless values', - }, - - // String negative unitless values - { - code: ` - import { style } from '@vanilla-extract/css'; - const myStyle = style({ - margin: '-10', - top: '-5.5', - }); - `, - errors: [ - { messageId: 'noUnitlessValue', data: { property: 'margin', value: '-10' } }, - { messageId: 'noUnitlessValue', data: { property: 'top', value: '-5.5' } }, - ], - name: 'should flag string negative unitless values', - }, - ], -}); diff --git a/src/css-rules/no-unitless-values/index.ts b/src/css-rules/no-unitless-values/index.ts deleted file mode 100644 index 235a28d..0000000 --- a/src/css-rules/no-unitless-values/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import noUnitlessValuesRule from './rule-definition.js'; - -export default noUnitlessValuesRule; diff --git a/src/css-rules/no-unitless-values/rule-definition.ts b/src/css-rules/no-unitless-values/rule-definition.ts deleted file mode 100644 index 032efd1..0000000 --- a/src/css-rules/no-unitless-values/rule-definition.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Rule } from 'eslint'; -import { createUnitlessValueVisitors } from './unitless-value-visitor-creator.js'; -import type { NoUnitlessValuesOptions } from './unitless-value-processor.js'; - -const noUnitlessValuesRule: Rule.RuleModule = { - meta: { - type: 'suggestion', - docs: { - description: 'disallow unitless numeric values for CSS properties that require units', - category: 'Stylistic Issues', - recommended: false, - }, - schema: [ - { - type: 'object', - properties: { - allow: { - type: 'array', - items: { - type: 'string', - }, - uniqueItems: true, - default: [], - }, - }, - additionalProperties: false, - }, - ], - messages: { - noUnitlessValue: - 'Property "{{ property }}" has unitless value {{ value }}. Add an explicit unit (e.g., "{{ value }}px", "{{ value }}rem").', - }, - }, - create(context) { - const options: NoUnitlessValuesOptions = (context.options[0] as NoUnitlessValuesOptions | undefined) || {}; - return createUnitlessValueVisitors(context, options); - }, -}; - -export default noUnitlessValuesRule; diff --git a/src/css-rules/no-unitless-values/unitless-value-processor.ts b/src/css-rules/no-unitless-values/unitless-value-processor.ts deleted file mode 100644 index 06055ed..0000000 --- a/src/css-rules/no-unitless-values/unitless-value-processor.ts +++ /dev/null @@ -1,371 +0,0 @@ -import type { Rule } from 'eslint'; -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; - -/** - * CSS properties that require units for length/dimension values. - * These properties should not have unitless numeric values (except 0). - */ -const PROPERTIES_REQUIRING_UNITS = new Set([ - // Box model - 'width', - 'height', - 'minWidth', - 'maxWidth', - 'minHeight', - 'maxHeight', - 'min-width', - 'max-width', - 'min-height', - 'max-height', - - // Spacing - 'margin', - 'marginTop', - 'marginRight', - 'marginBottom', - 'marginLeft', - 'marginBlock', - 'marginBlockStart', - 'marginBlockEnd', - 'marginInline', - 'marginInlineStart', - 'marginInlineEnd', - 'margin-top', - 'margin-right', - 'margin-bottom', - 'margin-left', - 'margin-block', - 'margin-block-start', - 'margin-block-end', - 'margin-inline', - 'margin-inline-start', - 'margin-inline-end', - - 'padding', - 'paddingTop', - 'paddingRight', - 'paddingBottom', - 'paddingLeft', - 'paddingBlock', - 'paddingBlockStart', - 'paddingBlockEnd', - 'paddingInline', - 'paddingInlineStart', - 'paddingInlineEnd', - 'padding-top', - 'padding-right', - 'padding-bottom', - 'padding-left', - 'padding-block', - 'padding-block-start', - 'padding-block-end', - 'padding-inline', - 'padding-inline-start', - 'padding-inline-end', - - // Positioning - 'top', - 'right', - 'bottom', - 'left', - 'inset', - 'insetBlock', - 'insetBlockStart', - 'insetBlockEnd', - 'insetInline', - 'insetInlineStart', - 'insetInlineEnd', - 'inset-block', - 'inset-block-start', - 'inset-block-end', - 'inset-inline', - 'inset-inline-start', - 'inset-inline-end', - - // Border - 'borderWidth', - 'borderTopWidth', - 'borderRightWidth', - 'borderBottomWidth', - 'borderLeftWidth', - 'borderBlockWidth', - 'borderBlockStartWidth', - 'borderBlockEndWidth', - 'borderInlineWidth', - 'borderInlineStartWidth', - 'borderInlineEndWidth', - 'border-width', - 'border-top-width', - 'border-right-width', - 'border-bottom-width', - 'border-left-width', - 'border-block-width', - 'border-block-start-width', - 'border-block-end-width', - 'border-inline-width', - 'border-inline-start-width', - 'border-inline-end-width', - - 'borderRadius', - 'borderTopLeftRadius', - 'borderTopRightRadius', - 'borderBottomLeftRadius', - 'borderBottomRightRadius', - 'borderStartStartRadius', - 'borderStartEndRadius', - 'borderEndStartRadius', - 'borderEndEndRadius', - 'border-radius', - 'border-top-left-radius', - 'border-top-right-radius', - 'border-bottom-left-radius', - 'border-bottom-right-radius', - 'border-start-start-radius', - 'border-start-end-radius', - 'border-end-start-radius', - 'border-end-end-radius', - - // Typography - 'fontSize', - 'font-size', - 'letterSpacing', - 'letter-spacing', - 'wordSpacing', - 'word-spacing', - 'textIndent', - 'text-indent', - - // Flexbox/Grid - 'gap', - 'rowGap', - 'columnGap', - 'row-gap', - 'column-gap', - 'flexBasis', - 'flex-basis', - - // Outline - 'outlineWidth', - 'outline-width', - 'outlineOffset', - 'outline-offset', - - // Other - 'blockSize', - 'inlineSize', - 'minBlockSize', - 'maxBlockSize', - 'minInlineSize', - 'maxInlineSize', - 'block-size', - 'inline-size', - 'min-block-size', - 'max-block-size', - 'min-inline-size', - 'max-inline-size', -]); - -/** - * CSS properties that accept unitless numeric values. - * These properties should NOT be flagged when they have numeric values. - */ -const UNITLESS_VALID_PROPERTIES = new Set([ - 'opacity', - 'zIndex', - 'z-index', - 'lineHeight', - 'line-height', - 'flexGrow', - 'flex-grow', - 'flexShrink', - 'flex-shrink', - 'order', - 'fontWeight', - 'font-weight', - 'zoom', - 'animationIterationCount', - 'animation-iteration-count', - 'columnCount', - 'column-count', - 'gridColumn', - 'grid-column', - 'gridColumnEnd', - 'grid-column-end', - 'gridColumnStart', - 'grid-column-start', - 'gridRow', - 'grid-row', - 'gridRowEnd', - 'grid-row-end', - 'gridRowStart', - 'grid-row-start', - 'orphans', - 'widows', - 'fillOpacity', - 'fill-opacity', - 'strokeOpacity', - 'stroke-opacity', - 'strokeMiterlimit', - 'stroke-miterlimit', -]); - -export interface NoUnitlessValuesOptions { - allow?: string[]; -} - -/** - * Checks if a property name requires units for numeric values. - */ -const requiresUnits = (propertyName: string, allow: string[] = []): boolean => { - if (allow.includes(propertyName)) { - return false; - } - - if (UNITLESS_VALID_PROPERTIES.has(propertyName)) { - return false; - } - - return PROPERTIES_REQUIRING_UNITS.has(propertyName); -}; - -/** - * Gets the property name from a Property node. - */ -const getPropertyName = (property: TSESTree.Property): string | null => { - if (property.key.type === AST_NODE_TYPES.Identifier) { - return property.key.name; - } - if (property.key.type === AST_NODE_TYPES.Literal && typeof property.key.value === 'string') { - return property.key.value; - } - return null; -}; - -/** - * Recursively processes a style object, reporting instances of unitless numeric values for properties that require units. - * - * @param ruleContext The ESLint rule context. - * @param node The ObjectExpression node representing the style object to be processed. - * @param options Rule options including allow list. - */ -export const processUnitlessValueInStyleObject = ( - ruleContext: Rule.RuleContext, - node: TSESTree.ObjectExpression, - options: NoUnitlessValuesOptions = {}, -): void => { - const allow = options.allow || []; - - node.properties.forEach((property) => { - if (property.type !== AST_NODE_TYPES.Property) { - return; - } - - const propertyName = getPropertyName(property); - if (!propertyName) { - return; - } - - // Skip special nested structures like @media, selectors, etc. - // These will be processed recursively - if (propertyName.startsWith('@') || propertyName.startsWith(':') || propertyName === 'selectors') { - if (property.value.type === AST_NODE_TYPES.ObjectExpression) { - if (propertyName === '@media' || propertyName === 'selectors') { - property.value.properties.forEach((nestedProperty) => { - if ( - nestedProperty.type === AST_NODE_TYPES.Property && - nestedProperty.value.type === AST_NODE_TYPES.ObjectExpression - ) { - processUnitlessValueInStyleObject(ruleContext, nestedProperty.value, options); - } - }); - } else { - // For pseudo-selectors and other nested objects, process directly - processUnitlessValueInStyleObject(ruleContext, property.value, options); - } - } - return; - } - - // Check if this property requires units - if (!requiresUnits(propertyName, allow)) { - // Still need to process nested objects for non-CSS properties - if (property.value.type === AST_NODE_TYPES.ObjectExpression) { - processUnitlessValueInStyleObject(ruleContext, property.value, options); - } - return; - } - - // Check for unitless numeric literal values (e.g., width: 100) - if (property.value.type === AST_NODE_TYPES.Literal && typeof property.value.value === 'number') { - // Allow 0 without units (valid CSS), including -0 - if (property.value.value === 0 || property.value.value === -0) { - return; - } - - // Report unitless numeric value - ruleContext.report({ - node: property.value, - messageId: 'noUnitlessValue', - data: { - property: propertyName, - value: String(property.value.value), - }, - }); - } - - // Check for string literals that are unitless numbers (e.g., width: '100') - if (property.value.type === AST_NODE_TYPES.Literal && typeof property.value.value === 'string') { - const stringValue = property.value.value.trim(); - - // Check if the string is a pure number (with optional negative sign and decimals) - // This regex matches: -10, 10, 10.5, -10.5, but not 10px, 10rem, etc. - const unitlessNumberRegex = /^-?\d+(\.\d+)?$/; - - if (unitlessNumberRegex.test(stringValue)) { - // Allow '0' and '-0' without units - const numValue = parseFloat(stringValue); - if (numValue === 0 || numValue === -0) { - return; - } - - // Report unitless string numeric value - ruleContext.report({ - node: property.value, - messageId: 'noUnitlessValue', - data: { - property: propertyName, - value: stringValue, - }, - }); - } - } - - // Check for unary expressions (e.g., -10) - if (property.value.type === AST_NODE_TYPES.UnaryExpression && property.value.operator === '-') { - if ( - property.value.argument.type === AST_NODE_TYPES.Literal && - typeof property.value.argument.value === 'number' - ) { - // Allow -0 without units - if (property.value.argument.value === 0) { - return; - } - - // Report unitless numeric value - ruleContext.report({ - node: property.value as unknown as Rule.Node, - messageId: 'noUnitlessValue', - data: { - property: propertyName, - value: `-${property.value.argument.value}`, - }, - }); - } - } - - // Process nested objects (for complex selectors, etc.) - if (property.value.type === AST_NODE_TYPES.ObjectExpression) { - processUnitlessValueInStyleObject(ruleContext, property.value, options); - } - }); -}; diff --git a/src/css-rules/no-unitless-values/unitless-value-visitor-creator.ts b/src/css-rules/no-unitless-values/unitless-value-visitor-creator.ts deleted file mode 100644 index 36b75fb..0000000 --- a/src/css-rules/no-unitless-values/unitless-value-visitor-creator.ts +++ /dev/null @@ -1,85 +0,0 @@ -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 { processUnitlessValueInStyleObject } from './unitless-value-processor.js'; -import type { NoUnitlessValuesOptions } from './unitless-value-processor.js'; - -/** - * Creates ESLint rule visitors for detecting unitless numeric values in style-related function calls. - * Uses reference tracking to automatically detect vanilla-extract functions based on their import statements. - * - * @param context The ESLint rule context. - * @param options Rule options including allowed property names. - * @returns An object with visitor functions for the ESLint rule. - */ -export const createUnitlessValueVisitors = ( - context: Rule.RuleContext, - options: NoUnitlessValuesOptions = {}, -): Rule.RuleListener => { - const tracker = new ReferenceTracker(); - const trackingVisitor = createReferenceTrackingVisitor(tracker); - - const processWithOptions = (ruleContext: Rule.RuleContext, node: TSESTree.ObjectExpression): void => { - processUnitlessValueInStyleObject(ruleContext, node, options); - }; - - return { - ...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) { - processWithOptions(context, node.arguments[0] as TSESTree.ObjectExpression); - } - break; - - case 'globalFontFace': - if (node.arguments.length > 1 && node.arguments[1]?.type === AST_NODE_TYPES.ObjectExpression) { - processWithOptions(context, node.arguments[1] as TSESTree.ObjectExpression); - } - break; - - case 'style': - case 'styleVariants': - case 'keyframes': - if (node.arguments.length > 0) { - processStyleNode(context, node.arguments[0] as TSESTree.ObjectExpression, processWithOptions); - } - break; - - case 'globalStyle': - case 'globalKeyframes': - if (node.arguments.length >= 2) { - processStyleNode(context, node.arguments[1] as TSESTree.ObjectExpression, processWithOptions); - } - break; - - case 'recipe': - if (node.arguments.length > 0 && node.arguments[0]?.type === AST_NODE_TYPES.ObjectExpression) { - processRecipeProperties(context, node.arguments[0] as TSESTree.ObjectExpression, processWithOptions); - } - break; - } - }, - }; -}; diff --git a/src/index.ts b/src/index.ts index 743ddaa..66bc0cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,6 @@ import customOrderRule from './css-rules/custom-order/rule-definition.js'; import noEmptyStyleBlocksRule from './css-rules/no-empty-blocks/rule-definition.js'; import noPxUnitRule from './css-rules/no-px-unit/index.js'; import noTrailingZeroRule from './css-rules/no-trailing-zero/rule-definition.js'; -import noUnitlessValuesRule from './css-rules/no-unitless-values/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'; import preferLogicalPropertiesRule from './css-rules/prefer-logical-properties/index.js'; @@ -13,7 +12,7 @@ import preferThemeTokensRule from './css-rules/prefer-theme-tokens/index.js'; const vanillaExtract = { meta: { name: '@antebudimir/eslint-plugin-vanilla-extract', - version: '1.16.0', + version: '1.15.1', }, rules: { 'alphabetical-order': alphabeticalOrderRule, @@ -22,7 +21,6 @@ const vanillaExtract = { 'no-empty-style-blocks': noEmptyStyleBlocksRule, 'no-px-unit': noPxUnitRule, 'no-trailing-zero': noTrailingZeroRule, - 'no-unitless-values': noUnitlessValuesRule, 'no-unknown-unit': noUnknownUnitRule, 'no-zero-unit': noZeroUnitRule, 'prefer-logical-properties': preferLogicalPropertiesRule,