mirror of
https://github.com/antebudimir/eslint-plugin-vanilla-extract.git
synced 2025-12-31 17:03:32 +00:00
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:
parent
f346002fb0
commit
175ce9aef8
45 changed files with 2674 additions and 566 deletions
142
src/css-rules/no-empty-blocks/recipe-processor.ts
Normal file
142
src/css-rules/no-empty-blocks/recipe-processor.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue