mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2026-01-01 17:23:31 +00:00
Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| d5c99de191 |
11 changed files with 8 additions and 1072 deletions
10
CHANGELOG.md
10
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
|
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).
|
[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
|
## [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
|
- 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
|
## [1.15.0] - 2025-11-14
|
||||||
|
|
|
||||||
77
README.md
77
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/alphabetical-order`: Alternative ordering rule (alphabetical sorting)
|
||||||
- `vanilla-extract/custom-order`: Alternative ordering rule (custom group-based 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-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-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)
|
- `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
|
### 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
|
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.
|
- `no-px-unit` rule to disallow use of `px` units with configurable whitelist.
|
||||||
- `prefer-logical-properties` rule to enforce use of logical properties.
|
- `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).
|
- `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
|
### Upcoming Features
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@antebudimir/eslint-plugin-vanilla-extract",
|
"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.",
|
"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",
|
"author": "Ante Budimir",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
|
|
@ -39,9 +39,6 @@ importers:
|
||||||
'@vanilla-extract/recipes':
|
'@vanilla-extract/recipes':
|
||||||
specifier: ^0.5.5
|
specifier: ^0.5.5
|
||||||
version: 0.5.5(@vanilla-extract/css@1.17.1)
|
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':
|
'@vitest/coverage-v8':
|
||||||
specifier: 3.0.8
|
specifier: 3.0.8
|
||||||
version: 3.0.8(vitest@3.0.8(@types/node@20.17.24)(tsx@4.19.3))
|
version: 3.0.8(vitest@3.0.8(@types/node@20.17.24)(tsx@4.19.3))
|
||||||
|
|
@ -614,11 +611,6 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@vanilla-extract/css': ^1.0.0
|
'@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':
|
'@vitest/coverage-v8@3.0.8':
|
||||||
resolution: {integrity: sha512-y7SAKsQirsEJ2F8bulBck4DoluhI2EEgTimHd6EEUgJBGKy9tC25cpywh1MH4FvDGoG2Unt7+asVd1kj4qOSAw==}
|
resolution: {integrity: sha512-y7SAKsQirsEJ2F8bulBck4DoluhI2EEgTimHd6EEUgJBGKy9tC25cpywh1MH4FvDGoG2Unt7+asVd1kj4qOSAw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -2326,10 +2318,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vanilla-extract/css': 1.17.1
|
'@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))':
|
'@vitest/coverage-v8@3.0.8(vitest@3.0.8(@types/node@20.17.24)(tsx@4.19.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1 @@
|
||||||
import noTrailingZeroRule from './rule-definition.js';
|
export { default } from './rule-definition.js';
|
||||||
|
|
||||||
export default noTrailingZeroRule;
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import noUnitlessValuesRule from './rule-definition.js';
|
|
||||||
|
|
||||||
export default noUnitlessValuesRule;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -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 noEmptyStyleBlocksRule from './css-rules/no-empty-blocks/rule-definition.js';
|
||||||
import noPxUnitRule from './css-rules/no-px-unit/index.js';
|
import noPxUnitRule from './css-rules/no-px-unit/index.js';
|
||||||
import noTrailingZeroRule from './css-rules/no-trailing-zero/rule-definition.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 noUnknownUnitRule from './css-rules/no-unknown-unit/rule-definition.js';
|
||||||
import noZeroUnitRule from './css-rules/no-zero-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';
|
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 = {
|
const vanillaExtract = {
|
||||||
meta: {
|
meta: {
|
||||||
name: '@antebudimir/eslint-plugin-vanilla-extract',
|
name: '@antebudimir/eslint-plugin-vanilla-extract',
|
||||||
version: '1.16.0',
|
version: '1.15.1',
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'alphabetical-order': alphabeticalOrderRule,
|
'alphabetical-order': alphabeticalOrderRule,
|
||||||
|
|
@ -22,7 +21,6 @@ const vanillaExtract = {
|
||||||
'no-empty-style-blocks': noEmptyStyleBlocksRule,
|
'no-empty-style-blocks': noEmptyStyleBlocksRule,
|
||||||
'no-px-unit': noPxUnitRule,
|
'no-px-unit': noPxUnitRule,
|
||||||
'no-trailing-zero': noTrailingZeroRule,
|
'no-trailing-zero': noTrailingZeroRule,
|
||||||
'no-unitless-values': noUnitlessValuesRule,
|
|
||||||
'no-unknown-unit': noUnknownUnitRule,
|
'no-unknown-unit': noUnknownUnitRule,
|
||||||
'no-zero-unit': noZeroUnitRule,
|
'no-zero-unit': noZeroUnitRule,
|
||||||
'prefer-logical-properties': preferLogicalPropertiesRule,
|
'prefer-logical-properties': preferLogicalPropertiesRule,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue