fix 🐞: fix false positives for non-empty object arguments in empty-style-blocks rule
Some checks failed
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled

This commit is contained in:
Ante Budimir 2025-11-22 12:13:26 +02:00
parent 1d88c12e3d
commit 7261c78a42
9 changed files with 478 additions and 7 deletions

View file

@ -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: [
// Empty recipe
@ -280,5 +398,185 @@ run({
`,
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' }],
},
],
});

View file

@ -1,6 +1,6 @@
import type { Rule } from 'eslint';
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 { processConditionalExpression } from './conditional-processor.js';
import { processEmptyNestedStyles } from './empty-nested-style-processor.js';
@ -58,7 +58,13 @@ export const isEffectivelyEmptyStylesObject = (stylesObject: TSESTree.ObjectExpr
if (propertyName === 'base') {
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;
}
} else if (propertyName === 'variants') {

View file

@ -1,6 +1,6 @@
import type { Rule } from 'eslint';
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 { removeNodeWithComma } from './node-remover.js';
import { areAllChildrenEmpty, getStyleKeyName } from './property-utils.js';
@ -88,8 +88,30 @@ export const processRecipeProperties = (
return;
}
// Check for non-object variant values
if (variantValueProperty.value.type !== 'ObjectExpression') {
const valueType = variantValueProperty.value.type;
// 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)) {
reportedNodes.add(variantValueProperty);