mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2025-12-31 08:53:33 +00:00
fix 🐞: fix false positives for non-empty object arguments in empty-style-blocks rule
This commit is contained in:
parent
1d88c12e3d
commit
7261c78a42
9 changed files with 478 additions and 7 deletions
|
|
@ -5,6 +5,11 @@ 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.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
|
||||||
|
- 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
|
||||||
|
|
||||||
- Add new rule `prefer-theme-tokens`that requires theme tokens instead of hard-coded values in vanilla-extract styles
|
- Add new rule `prefer-theme-tokens`that requires theme tokens instead of hard-coded values in vanilla-extract styles
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@antebudimir/eslint-plugin-vanilla-extract",
|
"name": "@antebudimir/eslint-plugin-vanilla-extract",
|
||||||
"version": "1.15.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",
|
||||||
|
|
@ -78,6 +78,7 @@
|
||||||
"@typescript-eslint/utils": "^8.26.1",
|
"@typescript-eslint/utils": "^8.26.1",
|
||||||
"@vanilla-extract/css": "^1.17.1",
|
"@vanilla-extract/css": "^1.17.1",
|
||||||
"@vanilla-extract/recipes": "^0.5.5",
|
"@vanilla-extract/recipes": "^0.5.5",
|
||||||
|
"@vanilla-extract/sprinkles": "^1.6.0",
|
||||||
"@vitest/coverage-v8": "3.0.8",
|
"@vitest/coverage-v8": "3.0.8",
|
||||||
"eslint": "^9.22.0",
|
"eslint": "^9.22.0",
|
||||||
"eslint-config-prettier": "^10.1.1",
|
"eslint-config-prettier": "^10.1.1",
|
||||||
|
|
|
||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
|
|
@ -39,6 +39,9 @@ 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))
|
||||||
|
|
@ -611,6 +614,11 @@ 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:
|
||||||
|
|
@ -2318,6 +2326,10 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,124 @@ run({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
// Recipe with sprinkles() in base - should be valid
|
||||||
|
`
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { display: ['flex'], flexDirection: ['row'], flexWrap: ['wrap-reverse'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const columnsStyle = recipe({
|
||||||
|
base: sprinkles({ display: 'flex', flexDirection: 'row' }),
|
||||||
|
variants: {
|
||||||
|
wrappingDirection: {
|
||||||
|
reverse: sprinkles({ flexWrap: 'wrap-reverse' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
|
||||||
|
// Recipe with sprinkles() in variant values - should be valid
|
||||||
|
`
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { padding: ['8px', '16px'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
spacing: {
|
||||||
|
small: sprinkles({ padding: '8px' }),
|
||||||
|
large: sprinkles({ padding: '16px' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
|
||||||
|
// Recipe with style() calls in variant values - should be valid
|
||||||
|
`
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
small: style({ fontSize: '12px' }),
|
||||||
|
large: style({ fontSize: '16px' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
|
||||||
|
// Recipe with mixed CallExpression and ObjectExpression in variants - should be valid
|
||||||
|
`
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { padding: ['8px'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
sprinkled: sprinkles({ padding: '8px' }),
|
||||||
|
regular: { padding: '8px' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
|
||||||
|
// Recipe with only CallExpression variants (no default) - should be valid
|
||||||
|
`
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { flexWrap: ['wrap-reverse'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
wrappingDirection: {
|
||||||
|
reverse: sprinkles({ flexWrap: 'wrap-reverse' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
|
||||||
|
// Recipe with nested CallExpression in multiple variant categories - should be valid
|
||||||
|
`
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { display: ['flex'], padding: ['8px', '16px'], color: ['blue', 'gray'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: sprinkles({ display: 'flex' }),
|
||||||
|
variants: {
|
||||||
|
spacing: {
|
||||||
|
small: sprinkles({ padding: '8px' }),
|
||||||
|
large: sprinkles({ padding: '16px' }),
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
primary: sprinkles({ color: 'blue' }),
|
||||||
|
secondary: sprinkles({ color: 'gray' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
],
|
],
|
||||||
invalid: [
|
invalid: [
|
||||||
// Empty recipe
|
// Empty recipe
|
||||||
|
|
@ -280,5 +398,185 @@ run({
|
||||||
`,
|
`,
|
||||||
errors: [{ messageId: 'invalidPropertyType', data: { type: 'ArrowFunctionExpression' } }],
|
errors: [{ messageId: 'invalidPropertyType', data: { type: 'ArrowFunctionExpression' } }],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Recipe with empty variant category alongside CallExpression variants
|
||||||
|
// Should only report the empty category, not the CallExpression
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { padding: ['8px', '16px'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
spacing: {
|
||||||
|
small: sprinkles({ padding: '8px' }),
|
||||||
|
large: sprinkles({ padding: '16px' }),
|
||||||
|
},
|
||||||
|
emptyCategory: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
errors: [{ messageId: 'emptyVariantCategory' }],
|
||||||
|
output: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { padding: ['8px', '16px'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
spacing: {
|
||||||
|
small: sprinkles({ padding: '8px' }),
|
||||||
|
large: sprinkles({ padding: '16px' }),
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Recipe with CallExpression in base and empty variants
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { display: ['flex'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: sprinkles({ display: 'flex' }),
|
||||||
|
variants: {},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
errors: [{ messageId: 'emptyRecipeProperty' }],
|
||||||
|
output: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { display: ['flex'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: sprinkles({ display: 'flex' }),
|
||||||
|
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Recipe with mixed valid CallExpression and invalid literal in same category
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { padding: ['8px'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
spacing: {
|
||||||
|
small: sprinkles({ padding: '8px' }),
|
||||||
|
invalid: 'invalid-string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
errors: [{ messageId: 'invalidPropertyType' }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Recipe with sprinkles({}) in base and empty variants - entire recipe is empty
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { display: ['flex'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const myRecipe = recipe({
|
||||||
|
base: sprinkles({}),
|
||||||
|
variants: {},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
errors: [{ messageId: 'emptyStyleDeclaration' }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Recipe with sprinkles({}) in variant value - should be flagged as empty
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { padding: ['8px'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
spacing: {
|
||||||
|
small: sprinkles({ padding: '8px' }),
|
||||||
|
empty: sprinkles({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
errors: [{ messageId: 'emptyVariantValue' }],
|
||||||
|
output: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { padding: ['8px'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
spacing: {
|
||||||
|
small: sprinkles({ padding: '8px' }),
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Recipe with both base and variants using empty CallExpressions - entire recipe becomes empty
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { display: ['flex'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const myRecipe = recipe({
|
||||||
|
base: style({}),
|
||||||
|
variants: {
|
||||||
|
layout: {
|
||||||
|
flex: sprinkles({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
errors: [{ messageId: 'emptyStyleDeclaration' }, { messageId: 'emptyVariantValue' }],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Rule } from 'eslint';
|
import type { Rule } from 'eslint';
|
||||||
import { TSESTree } from '@typescript-eslint/utils';
|
import { TSESTree } from '@typescript-eslint/utils';
|
||||||
import { isEmptyObject } from '../shared-utils/empty-object-processor.js';
|
import { isCallExpressionWithEmptyObject, isEmptyObject } from '../shared-utils/empty-object-processor.js';
|
||||||
import { ReferenceTracker, createReferenceTrackingVisitor } from '../shared-utils/reference-tracker.js';
|
import { ReferenceTracker, createReferenceTrackingVisitor } from '../shared-utils/reference-tracker.js';
|
||||||
import { processConditionalExpression } from './conditional-processor.js';
|
import { processConditionalExpression } from './conditional-processor.js';
|
||||||
import { processEmptyNestedStyles } from './empty-nested-style-processor.js';
|
import { processEmptyNestedStyles } from './empty-nested-style-processor.js';
|
||||||
|
|
@ -58,7 +58,13 @@ export const isEffectivelyEmptyStylesObject = (stylesObject: TSESTree.ObjectExpr
|
||||||
|
|
||||||
if (propertyName === 'base') {
|
if (propertyName === 'base') {
|
||||||
hasBaseProperty = true;
|
hasBaseProperty = true;
|
||||||
if (property.value.type === 'ObjectExpression' && !isEmptyObject(property.value)) {
|
|
||||||
|
// CallExpression (e.g., sprinkles(), style()) is considered non-empty unless it has an empty object argument, e.g. sprinkles({})
|
||||||
|
if (property.value.type === 'CallExpression') {
|
||||||
|
if (!isCallExpressionWithEmptyObject(property.value)) {
|
||||||
|
isBaseEmpty = false;
|
||||||
|
}
|
||||||
|
} else if (property.value.type === 'ObjectExpression' && !isEmptyObject(property.value)) {
|
||||||
isBaseEmpty = false;
|
isBaseEmpty = false;
|
||||||
}
|
}
|
||||||
} else if (propertyName === 'variants') {
|
} else if (propertyName === 'variants') {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Rule } from 'eslint';
|
import type { Rule } from 'eslint';
|
||||||
import { TSESTree } from '@typescript-eslint/utils';
|
import { TSESTree } from '@typescript-eslint/utils';
|
||||||
import { isEmptyObject } from '../shared-utils/empty-object-processor.js';
|
import { isCallExpressionWithEmptyObject, isEmptyObject } from '../shared-utils/empty-object-processor.js';
|
||||||
import { processEmptyNestedStyles } from './empty-nested-style-processor.js';
|
import { processEmptyNestedStyles } from './empty-nested-style-processor.js';
|
||||||
import { removeNodeWithComma } from './node-remover.js';
|
import { removeNodeWithComma } from './node-remover.js';
|
||||||
import { areAllChildrenEmpty, getStyleKeyName } from './property-utils.js';
|
import { areAllChildrenEmpty, getStyleKeyName } from './property-utils.js';
|
||||||
|
|
@ -88,8 +88,30 @@ export const processRecipeProperties = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for non-object variant values
|
const valueType = variantValueProperty.value.type;
|
||||||
if (variantValueProperty.value.type !== 'ObjectExpression') {
|
|
||||||
|
// Allow CallExpression (e.g., sprinkles(), style()) as valid variant values unless it has an empty object argument, e.g. sprinkles({})
|
||||||
|
if (valueType === 'CallExpression') {
|
||||||
|
const callExpression = variantValueProperty.value as TSESTree.CallExpression;
|
||||||
|
if (isCallExpressionWithEmptyObject(callExpression)) {
|
||||||
|
// Treat sprinkles({}) or style({}) as empty
|
||||||
|
if (!reportedNodes.has(variantValueProperty)) {
|
||||||
|
reportedNodes.add(variantValueProperty);
|
||||||
|
ruleContext.report({
|
||||||
|
node: variantValueProperty as Rule.Node,
|
||||||
|
messageId: 'emptyVariantValue',
|
||||||
|
fix(fixer) {
|
||||||
|
return removeNodeWithComma(ruleContext, variantValueProperty, fixer);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Valid CallExpressions with arguments are fine
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for non-object variant values (excluding CallExpression)
|
||||||
|
if (valueType !== 'ObjectExpression') {
|
||||||
if (!reportedNodes.has(variantValueProperty)) {
|
if (!reportedNodes.has(variantValueProperty)) {
|
||||||
reportedNodes.add(variantValueProperty);
|
reportedNodes.add(variantValueProperty);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import { run } from 'eslint-vitest-rule-tester';
|
||||||
|
import noEmptyBlocksRule from '../../no-empty-blocks/rule-definition.js';
|
||||||
|
|
||||||
|
run({
|
||||||
|
name: 'vanilla-extract/empty-object-processor-tests',
|
||||||
|
rule: noEmptyBlocksRule,
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
valid: [
|
||||||
|
// sprinkles() with no arguments is valid (not empty)
|
||||||
|
`
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { display: ['flex'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
value: sprinkles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
|
||||||
|
// Test for CallExpression with non-empty object argument
|
||||||
|
`
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { padding: ['8px'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
spacing: {
|
||||||
|
small: sprinkles({ padding: '8px' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
// Test for CallExpression with empty object argument - sprinkles({})
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { padding: ['8px'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
spacing: {
|
||||||
|
small: sprinkles({ padding: '8px' }),
|
||||||
|
empty: sprinkles({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
errors: [{ messageId: 'emptyVariantValue' }],
|
||||||
|
output: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { padding: ['8px'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const myRecipe = recipe({
|
||||||
|
base: { color: 'black' },
|
||||||
|
variants: {
|
||||||
|
spacing: {
|
||||||
|
small: sprinkles({ padding: '8px' }),
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test for sprinkles({}) with empty object argument in recipe base
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
|
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles';
|
||||||
|
|
||||||
|
const sprinkles = createSprinkles(defineProperties({
|
||||||
|
properties: { display: ['flex'] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const myRecipe = recipe({
|
||||||
|
base: sprinkles({}),
|
||||||
|
variants: {},
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
errors: [{ messageId: 'emptyStyleDeclaration' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
@ -15,3 +15,16 @@ export const isObjectExpression = (node: TSESTree.Node): node is TSESTree.Object
|
||||||
export const isEmptyObject = (node: TSESTree.Node): boolean => {
|
export const isEmptyObject = (node: TSESTree.Node): boolean => {
|
||||||
return isObjectExpression(node) && node.properties.length === 0;
|
return isObjectExpression(node) && node.properties.length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a CallExpression has an empty object as its first argument.
|
||||||
|
* Examples: sprinkles({}), style({}), recipe({})
|
||||||
|
*/
|
||||||
|
export const isCallExpressionWithEmptyObject = (node: TSESTree.CallExpression): boolean => {
|
||||||
|
if (node.arguments.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstArgument = node.arguments[0];
|
||||||
|
return firstArgument?.type === 'ObjectExpression' && isEmptyObject(firstArgument);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,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.15.0',
|
version: '1.15.1',
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'alphabetical-order': alphabeticalOrderRule,
|
'alphabetical-order': alphabeticalOrderRule,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue