feat 🥁: add no-empty-style-blocks rule

Add comprehensive rule to detect and prevent empty CSS style blocks:

- Identify style objects with no properties
- Flag empty style blocks as potential code quality issues
- Provide auto-fix capability to remove empty blocks
- Handle edge cases like comments-only blocks

This rule helps maintain cleaner codebases by eliminating empty style definitions that often result from incomplete refactoring or forgotten implementations, reducing confusion and unnecessary code.
This commit is contained in:
Ante Budimir 2025-04-06 11:37:34 +03:00
parent f346002fb0
commit 175ce9aef8
45 changed files with 2674 additions and 566 deletions

View file

@ -0,0 +1,142 @@
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 function 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);
}
});
});
}
}
});
}