eslint-plugin-vanilla-extract/src/css-rules/no-empty-blocks/recipe-processor.ts
Ante Budimir 7dc7204749 feat 🥁: add no-zero-unit rule
This rule enforces unitless zero values in vanilla-extract style objects:
- Automatically removes unnecessary units from zero values
- Handles both positive and negative zero values
- Preserves units where required (time properties, CSS functions)
- Works with all vanilla-extract APIs
2025-04-12 20:53:34 +03:00

142 lines
5.1 KiB
TypeScript

import type { Rule } from 'eslint';
import { TSESTree } from '@typescript-eslint/utils';
import { 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';
/**
* Processes a recipe object, removing empty `base` and `variants` properties, as well as empty
* variant categories and values.
*
* @param ruleContext The ESLint rule context.
* @param recipeNode The recipe object node to process.
* @param reportedNodes A set of nodes that have already been reported by other processors.
*/
export const processRecipeProperties = (
ruleContext: Rule.RuleContext,
recipeNode: TSESTree.ObjectExpression,
reportedNodes: Set<TSESTree.Node>,
): void => {
recipeNode.properties.forEach((property) => {
if (property.type !== 'Property') {
return;
}
const propertyName = getStyleKeyName(property.key);
if (!propertyName) {
return;
}
// Handle empty base or variants properties
if (
(propertyName === 'base' || propertyName === 'variants') &&
property.value.type === 'ObjectExpression' &&
isEmptyObject(property.value)
) {
if (!reportedNodes.has(property)) {
reportedNodes.add(property);
ruleContext.report({
node: property as Rule.Node,
messageId: 'emptyRecipeProperty',
data: {
propertyName,
},
fix(fixer) {
return removeNodeWithComma(ruleContext, property, fixer);
},
});
}
}
// Process base property nested objects
if (propertyName === 'base' && property.value.type === 'ObjectExpression') {
processEmptyNestedStyles(ruleContext, property.value, reportedNodes);
}
// Process variant values
if (propertyName === 'variants' && property.value.type === 'ObjectExpression') {
// If variants is empty, it will be handled by the check above
if (!isEmptyObject(property.value)) {
// Process variant categories
property.value.properties.forEach((variantCategoryProperty) => {
if (
variantCategoryProperty.type !== 'Property' ||
variantCategoryProperty.value.type !== 'ObjectExpression'
) {
return;
}
// Check if all values in this category are empty
if (isEmptyObject(variantCategoryProperty.value) || areAllChildrenEmpty(variantCategoryProperty.value)) {
if (!reportedNodes.has(variantCategoryProperty)) {
reportedNodes.add(variantCategoryProperty);
ruleContext.report({
node: variantCategoryProperty as Rule.Node,
messageId: 'emptyVariantCategory',
fix(fixer) {
return removeNodeWithComma(ruleContext, variantCategoryProperty, fixer);
},
});
}
return;
}
// Process individual variant values
variantCategoryProperty.value.properties.forEach((variantValueProperty) => {
if (variantValueProperty.type !== 'Property') {
return;
}
// Check for non-object variant values
if (variantValueProperty.value.type !== 'ObjectExpression') {
if (!reportedNodes.has(variantValueProperty)) {
reportedNodes.add(variantValueProperty);
// Get a user-friendly type description as a string
const friendlyType = (() => {
const nodeType = variantValueProperty.value.type;
if (nodeType === 'Literal') {
const literalValue = variantValueProperty.value as TSESTree.Literal;
return literalValue.value === null ? 'null' : typeof literalValue.value;
} else if (nodeType === 'Identifier') {
return 'variable';
}
return nodeType;
})();
ruleContext.report({
node: variantValueProperty as Rule.Node,
messageId: 'invalidPropertyType',
data: {
type: friendlyType,
},
});
}
return;
}
// Check for empty objects in variant properties
if (isEmptyObject(variantValueProperty.value)) {
if (!reportedNodes.has(variantValueProperty)) {
reportedNodes.add(variantValueProperty);
ruleContext.report({
node: variantValueProperty as Rule.Node,
messageId: 'emptyVariantValue',
fix(fixer) {
return removeNodeWithComma(ruleContext, variantValueProperty, fixer);
},
});
}
} else {
// Process nested styles within variant values
processEmptyNestedStyles(ruleContext, variantValueProperty.value, reportedNodes);
}
});
});
}
}
});
};