mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2026-01-01 17:23:31 +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
|
|
@ -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' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue