refactor ♻️: improve code quality and test coverage

- Fix handling of missing groupOrder configuration
- Refactor negative conditions to positive ones with optional chaining
- Add comprehensive tests to achieve total coverage
This commit is contained in:
Ante Budimir 2025-03-12 06:06:40 +02:00
parent 5557409368
commit 46751da51b
15 changed files with 310 additions and 186 deletions

View file

@ -0,0 +1,115 @@
import tsParser from '@typescript-eslint/parser';
import { run } from 'eslint-vitest-rule-tester';
import customGroupOrderRule from '../rule-definition.js';
run({
name: 'vanilla-extract/custom-order/defaults',
rule: customGroupOrderRule,
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
},
valid: [
// Test with no options provided - should use defaults
`
import { style } from '@vanilla-extract/css';
const myStyle = style({
alignItems: 'center',
backgroundColor: 'red',
color: 'blue',
display: 'flex',
margin: '10px',
padding: '20px',
zIndex: 1
});
`,
// Test with empty groupOrder array - should use defaults
{
code: `
import { style } from '@vanilla-extract/css';
const myStyle = style({
alignItems: 'center',
backgroundColor: 'red',
color: 'blue',
display: 'flex',
margin: '10px',
padding: '20px',
zIndex: 1
});
`,
options: [
{
groupOrder: [],
},
],
},
],
invalid: [
// Test with no options provided - should use alphabetical ordering by default
{
code: `
import { style } from '@vanilla-extract/css';
const myStyle = style({
zIndex: 1,
padding: '20px',
margin: '10px',
display: 'flex',
color: 'blue',
backgroundColor: 'red',
alignItems: 'center'
});
`,
errors: [{ messageId: 'alphabeticalOrder' }],
output: `
import { style } from '@vanilla-extract/css';
const myStyle = style({
alignItems: 'center',
backgroundColor: 'red',
color: 'blue',
display: 'flex',
margin: '10px',
padding: '20px',
zIndex: 1
});
`,
},
// Test with empty groupOrder array - should use alphabetical ordering by default
{
code: `
import { style } from '@vanilla-extract/css';
const myStyle = style({
zIndex: 1,
padding: '20px',
margin: '10px',
display: 'flex',
color: 'blue',
backgroundColor: 'red',
alignItems: 'center'
});
`,
options: [
{
groupOrder: [],
},
],
errors: [{ messageId: 'alphabeticalOrder' }],
output: `
import { style } from '@vanilla-extract/css';
const myStyle = style({
alignItems: 'center',
backgroundColor: 'red',
color: 'blue',
display: 'flex',
margin: '10px',
padding: '20px',
zIndex: 1
});
`,
},
],
});

View file

@ -23,69 +23,74 @@ export const enforceCustomGroupOrder = (
userDefinedGroups: string[] = [],
sortRemainingProperties?: 'alphabetical' | 'concentric',
): void => {
if (cssPropertyInfoList.length <= 1) {
return;
}
if (cssPropertyInfoList.length > 1) {
const cssPropertyPriorityMap = createCSSPropertyPriorityMap(userDefinedGroups);
const cssPropertyPriorityMap = createCSSPropertyPriorityMap(userDefinedGroups);
const compareProperties = (firstProperty: CSSPropertyInfo, secondProperty: CSSPropertyInfo) => {
const firstPropertyInfo = cssPropertyPriorityMap.get(firstProperty.name) || {
groupIndex: Infinity,
positionInGroup: Infinity,
inUserGroup: false,
};
const secondPropertyInfo = cssPropertyPriorityMap.get(secondProperty.name) || {
groupIndex: Infinity,
positionInGroup: Infinity,
inUserGroup: false,
};
const compareProperties = (firstProperty: CSSPropertyInfo, secondProperty: CSSPropertyInfo) => {
const firstPropertyInfo = cssPropertyPriorityMap.get(firstProperty.name) || {
groupIndex: Infinity,
positionInGroup: Infinity,
inUserGroup: false,
};
const secondPropertyInfo = cssPropertyPriorityMap.get(secondProperty.name) || {
groupIndex: Infinity,
positionInGroup: Infinity,
inUserGroup: false,
};
if (firstPropertyInfo.inUserGroup !== secondPropertyInfo.inUserGroup) {
return firstPropertyInfo.inUserGroup ? -1 : 1;
}
if (firstPropertyInfo.inUserGroup) {
if (firstPropertyInfo.groupIndex !== secondPropertyInfo.groupIndex) {
return firstPropertyInfo.groupIndex - secondPropertyInfo.groupIndex;
if (firstPropertyInfo.inUserGroup !== secondPropertyInfo.inUserGroup) {
return firstPropertyInfo.inUserGroup ? -1 : 1;
}
return firstPropertyInfo.positionInGroup - secondPropertyInfo.positionInGroup;
if (firstPropertyInfo.inUserGroup) {
if (firstPropertyInfo.groupIndex !== secondPropertyInfo.groupIndex) {
return firstPropertyInfo.groupIndex - secondPropertyInfo.groupIndex;
}
return firstPropertyInfo.positionInGroup - secondPropertyInfo.positionInGroup;
}
// For properties not in user-defined groups
if (sortRemainingProperties === 'alphabetical') {
return firstProperty.name.localeCompare(secondProperty.name);
} else {
return (
firstPropertyInfo.groupIndex - secondPropertyInfo.groupIndex ||
firstPropertyInfo.positionInGroup - secondPropertyInfo.positionInGroup
);
}
};
const sortedPropertyList = [...cssPropertyInfoList].sort(compareProperties);
// Find the first pair that violates the new ordering
const violatingProperty = cssPropertyInfoList
.slice(0, -1)
.find((currentProperty, index) => currentProperty.name !== sortedPropertyList[index]?.name);
if (violatingProperty) {
const indexInSorted = cssPropertyInfoList.indexOf(violatingProperty);
const sortedProperty = sortedPropertyList[indexInSorted];
// Defensive programming - sortedProperty will always exist and have a name because sortedPropertyList is derived from cssPropertyInfoList and cssPropertyInfoList exists and is non-empty
// Therefore, we can exclude the next line from coverage because it's unreachable in practice
/* v8 ignore next */
const nextPropertyName = sortedProperty?.name ?? '';
ruleContext.report({
node: violatingProperty.node as Rule.Node,
messageId: 'incorrectOrder',
data: {
currentProperty: violatingProperty.name,
nextProperty: nextPropertyName,
},
fix: (fixer) =>
generateFixesForCSSOrder(
fixer,
ruleContext,
cssPropertyInfoList,
compareProperties,
(propertyInfo) => propertyInfo.node as Rule.Node,
),
});
}
// For properties not in user-defined groups
if (sortRemainingProperties === 'alphabetical') {
return firstProperty.name.localeCompare(secondProperty.name);
} else {
return (
firstPropertyInfo.groupIndex - secondPropertyInfo.groupIndex ||
firstPropertyInfo.positionInGroup - secondPropertyInfo.positionInGroup
);
}
};
const sortedPropertyList = [...cssPropertyInfoList].sort(compareProperties);
// Find the first pair that violates the new ordering
const violatingProperty = cssPropertyInfoList
.slice(0, -1)
.find((currentProperty, index) => currentProperty.name !== sortedPropertyList[index]?.name);
if (violatingProperty) {
ruleContext.report({
node: violatingProperty.node as Rule.Node,
messageId: 'incorrectOrder',
data: {
currentProperty: violatingProperty.name,
nextProperty: sortedPropertyList[cssPropertyInfoList.indexOf(violatingProperty)]?.name || '',
},
fix: (fixer) =>
generateFixesForCSSOrder(
fixer,
ruleContext,
cssPropertyInfoList,
compareProperties,
(propertyInfo) => propertyInfo.node as Rule.Node,
),
});
}
};

View file

@ -23,20 +23,18 @@ export const enforceUserDefinedGroupOrderInRecipe = (
userDefinedGroups: string[],
sortRemainingPropertiesMethod?: 'alphabetical' | 'concentric',
): void => {
if (!callExpression.arguments[0] || callExpression.arguments[0].type !== 'ObjectExpression') {
return;
}
if (callExpression.arguments[0]?.type === 'ObjectExpression') {
const recipeObjectExpression = callExpression.arguments[0];
const recipeObjectExpression = callExpression.arguments[0];
processRecipeProperties(ruleContext, recipeObjectExpression, (currentContext, styleObject) =>
processStyleNode(currentContext, styleObject, (styleContext, styleObjectNode) =>
enforceUserDefinedGroupOrderInStyleObject(
styleContext,
styleObjectNode,
userDefinedGroups,
sortRemainingPropertiesMethod,
processRecipeProperties(ruleContext, recipeObjectExpression, (currentContext, styleObject) =>
processStyleNode(currentContext, styleObject, (styleContext, styleObjectNode) =>
enforceUserDefinedGroupOrderInStyleObject(
styleContext,
styleObjectNode,
userDefinedGroups,
sortRemainingPropertiesMethod,
),
),
),
);
);
}
};

View file

@ -34,10 +34,13 @@ const customGroupOrderRule: Rule.RuleModule = {
},
],
messages: {
incorrectOrder:
"Property '{{nextProperty}}' should come before '{{currentProperty}}' according to custom CSS group ordering.",
// default message for ordering in case no groupOrder is provided
alphabeticalOrder:
"Property '{{nextProperty}}' should come before '{{currentProperty}}' according to alphabetical ordering.",
fontFaceOrder:
"Properties in fontFace should be ordered with 'src' first, followed by other properties in alphabetical order. Property '{{nextProperty}}' should come before '{{currentProperty}}'.",
incorrectOrder:
"Property '{{nextProperty}}' should come before '{{currentProperty}}' according to custom CSS group ordering.",
},
},
create(ruleContext: Rule.RuleContext) {

View file

@ -29,51 +29,49 @@ export const enforceUserDefinedGroupOrderInStyleObject = (
userDefinedGroups: string[],
sortRemainingPropertiesMethod: 'alphabetical' | 'concentric' = 'concentric',
): void => {
if (!styleObject || styleObject.type !== AST_NODE_TYPES.ObjectExpression) {
return;
}
if (styleObject?.type === AST_NODE_TYPES.ObjectExpression) {
if (isSelectorsObject(styleObject)) {
styleObject.properties.forEach((property) => {
if (property.type === AST_NODE_TYPES.Property && property.value.type === AST_NODE_TYPES.ObjectExpression) {
enforceUserDefinedGroupOrderInStyleObject(
ruleContext,
property.value,
userDefinedGroups,
sortRemainingPropertiesMethod,
);
}
});
return;
}
if (isSelectorsObject(styleObject)) {
styleObject.properties.forEach((property) => {
if (property.type === AST_NODE_TYPES.Property && property.value.type === AST_NODE_TYPES.ObjectExpression) {
enforceUserDefinedGroupOrderInStyleObject(
ruleContext,
property.value,
userDefinedGroups,
sortRemainingPropertiesMethod,
);
}
const cssPropertyPriorityMap = createCSSPropertyPriorityMap(userDefinedGroups);
const { regularProperties } = separateProperties(styleObject.properties);
const cssPropertyInfoList: CSSPropertyInfo[] = regularProperties.map((property) => {
const propertyName = getPropertyName(property);
const propertyInfo = cssPropertyPriorityMap.get(propertyName);
const group =
userDefinedGroups.find((groupName) => concentricGroups[groupName]?.includes(propertyName)) || 'remaining';
return {
name: propertyName,
node: property,
priority: propertyInfo?.groupIndex ?? Number.MAX_SAFE_INTEGER,
positionInGroup: propertyInfo?.positionInGroup ?? Number.MAX_SAFE_INTEGER,
group,
inUserGroup: propertyInfo?.inUserGroup ?? false,
};
});
return;
enforceCustomGroupOrder(ruleContext, cssPropertyInfoList, userDefinedGroups, sortRemainingPropertiesMethod);
processNestedSelectors(ruleContext, styleObject, (nestedContext, nestedNode) =>
enforceUserDefinedGroupOrderInStyleObject(
nestedContext,
nestedNode,
userDefinedGroups,
sortRemainingPropertiesMethod,
),
);
}
const cssPropertyPriorityMap = createCSSPropertyPriorityMap(userDefinedGroups);
const { regularProperties } = separateProperties(styleObject.properties);
const cssPropertyInfoList: CSSPropertyInfo[] = regularProperties.map((property) => {
const propertyName = getPropertyName(property);
const propertyInfo = cssPropertyPriorityMap.get(propertyName);
const group =
userDefinedGroups.find((groupName) => concentricGroups[groupName]?.includes(propertyName)) || 'remaining';
return {
name: propertyName,
node: property,
priority: propertyInfo?.groupIndex ?? Number.MAX_SAFE_INTEGER,
positionInGroup: propertyInfo?.positionInGroup ?? Number.MAX_SAFE_INTEGER,
group,
inUserGroup: propertyInfo?.inUserGroup ?? false,
};
});
enforceCustomGroupOrder(ruleContext, cssPropertyInfoList, userDefinedGroups, sortRemainingPropertiesMethod);
processNestedSelectors(ruleContext, styleObject, (nestedContext, nestedNode) =>
enforceUserDefinedGroupOrderInStyleObject(
nestedContext,
nestedNode,
userDefinedGroups,
sortRemainingPropertiesMethod,
),
);
};