mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2026-04-16 00:37:24 +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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
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 = {
|
||||
meta: {
|
||||
name: '@antebudimir/eslint-plugin-vanilla-extract',
|
||||
version: '1.15.0',
|
||||
version: '1.15.1',
|
||||
},
|
||||
rules: {
|
||||
'alphabetical-order': alphabeticalOrderRule,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue