mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2025-12-31 17:03:32 +00:00
Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| 62b1844b44 |
10 changed files with 1059 additions and 7 deletions
|
|
@ -5,6 +5,14 @@ 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/7) to prevent false positives for `sprinkles()`/`style()`/`recipe()` calls with non-empty object arguments while continuing to flag bare `({})` calls
|
||||||
|
|
|
||||||
77
README.md
77
README.md
|
|
@ -264,6 +264,7 @@ 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)
|
||||||
|
|
||||||
|
|
@ -536,6 +537,77 @@ 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
|
||||||
|
|
@ -835,10 +907,7 @@ 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.15.1",
|
"version": "1.16.0",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
export { default } from './rule-definition.js';
|
import noTrailingZeroRule from './rule-definition.js';
|
||||||
|
|
||||||
|
export default noTrailingZeroRule;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,472 @@
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
3
src/css-rules/no-unitless-values/index.ts
Normal file
3
src/css-rules/no-unitless-values/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import noUnitlessValuesRule from './rule-definition.js';
|
||||||
|
|
||||||
|
export default noUnitlessValuesRule;
|
||||||
40
src/css-rules/no-unitless-values/rule-definition.ts
Normal file
40
src/css-rules/no-unitless-values/rule-definition.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
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;
|
||||||
371
src/css-rules/no-unitless-values/unitless-value-processor.ts
Normal file
371
src/css-rules/no-unitless-values/unitless-value-processor.ts
Normal file
|
|
@ -0,0 +1,371 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
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,6 +4,7 @@ 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';
|
||||||
|
|
@ -12,7 +13,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.15.1',
|
version: '1.16.0',
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'alphabetical-order': alphabeticalOrderRule,
|
'alphabetical-order': alphabeticalOrderRule,
|
||||||
|
|
@ -21,6 +22,7 @@ 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