feat 🥁: add wrapper function support with reference tracking

- add reference tracking for wrapper functions in vanilla-extract style objects
- implement ReferenceTracker class for detecting vanilla-extract imports
- add createReferenceBasedNodeVisitors for automatic function detection
- support wrapper functions with parameter mapping enable all lint rules to work with custom wrapper functions

This commit introduces robust reference tracking and wrapper function support, enabling all lint rules to work seamlessly with custom vanilla-extract style patterns while preserving compatibility with existing usage and improving rule extensibility.
This commit is contained in:
Seongmin Choi 2025-06-26 01:51:36 +09:00 committed by GitHub
parent 35875fbb31
commit 02576d923c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1942 additions and 212 deletions

View file

@ -7,25 +7,20 @@ import { enforceConcentricCSSOrderInStyleObject } from '../concentric-order/styl
import { enforceUserDefinedGroupOrderInRecipe } from '../custom-order/recipe-order-enforcer.js';
import { enforceUserDefinedGroupOrderInStyleObject } from '../custom-order/style-object-processor.js';
import { enforceFontFaceOrder } from './font-face-property-order-enforcer.js';
import { ReferenceTracker, createReferenceTrackingVisitor } from './reference-tracker.js';
import { processStyleNode } from './style-node-processor.js';
import type { SortRemainingProperties } from '../concentric-order/types.js';
import type { OrderingStrategy } from '../types.js';
/**
* Creates an ESLint rule listener with visitors for style-related function calls.
* Creates an ESLint rule listener with visitors for style-related function calls using reference tracking.
* This automatically detects vanilla-extract functions based on their import statements.
*
* @param ruleContext The ESLint rule context.
* @param orderingStrategy The strategy to use for ordering CSS properties ('alphabetical', 'concentric', or 'userDefinedGroupOrder').
* @param userDefinedGroupOrder An optional array of property groups for the 'userDefinedGroupOrder' strategy.
* @param sortRemainingProperties An optional strategy for sorting properties not in user-defined groups.
* @returns An object with visitor functions for the ESLint rule.
*
* This function sets up visitors for the following cases:
* 1. The fontFace and globalFontFace functions.
* 2. Style-related functions: 'keyframes', 'style', 'styleVariants'.
* 3. The 'globalStyle' and 'globalKeyframes' function
* 4. The 'recipe' function
*
* Each visitor applies the appropriate ordering strategy to the style objects in these function calls.
*/
export const createNodeVisitors = (
ruleContext: Rule.RuleContext,
@ -33,88 +28,157 @@ export const createNodeVisitors = (
userDefinedGroupOrder?: string[],
sortRemainingProperties?: SortRemainingProperties,
): Rule.RuleListener => {
// Select the appropriate property processing function based on the ordering strategy
const processProperty = (() => {
switch (orderingStrategy) {
case 'alphabetical':
return enforceAlphabeticalCSSOrderInStyleObject;
case 'concentric':
return enforceConcentricCSSOrderInStyleObject;
case 'userDefinedGroupOrder':
if (!userDefinedGroupOrder || userDefinedGroupOrder.length === 0) {
return enforceAlphabeticalCSSOrderInStyleObject;
}
return (ruleContext: Rule.RuleContext, node: TSESTree.ObjectExpression) =>
enforceUserDefinedGroupOrderInStyleObject(ruleContext, node, userDefinedGroupOrder, sortRemainingProperties);
default:
return enforceAlphabeticalCSSOrderInStyleObject;
}
})();
const tracker = new ReferenceTracker();
const trackingVisitor = createReferenceTrackingVisitor(tracker);
return {
// Include the import/variable tracking visitors
...trackingVisitor,
CallExpression(node) {
if (node.callee.type !== 'Identifier') {
return;
}
const fontFaceFunctionArgumentIndexMap = {
fontFace: 0, // First argument (index 0)
globalFontFace: 1, // Second argument (index 1)
};
// Handle font face functions with special ordering
if (
node.callee.name in fontFaceFunctionArgumentIndexMap &&
node.arguments.length >
fontFaceFunctionArgumentIndexMap[node.callee.name as keyof typeof fontFaceFunctionArgumentIndexMap]
) {
const argumentIndex =
fontFaceFunctionArgumentIndexMap[node.callee.name as keyof typeof fontFaceFunctionArgumentIndexMap];
const styleArguments = node.arguments[argumentIndex];
enforceFontFaceOrder(ruleContext, styleArguments as TSESTree.ObjectExpression);
const functionName = node.callee.name;
// Check if this function is tracked as a vanilla-extract function
if (!tracker.isTrackedFunction(functionName)) {
return;
}
// Handle style-related functions
if (['keyframes', 'style', 'styleVariants'].includes(node.callee.name)) {
if (node.arguments.length > 0) {
const styleArguments = node.arguments[0];
processStyleNode(ruleContext, styleArguments as TSESTree.Node, processProperty);
}
const originalName = tracker.getOriginalName(functionName);
if (!originalName) {
return;
}
// Handle global functions
if (
(node.callee.name === 'globalKeyframes' || node.callee.name === 'globalStyle') &&
node.arguments.length >= 2
) {
const styleArguments = node.arguments[1];
processStyleNode(ruleContext, styleArguments as TSESTree.Node, processProperty);
}
// Handle different function types based on their original imported name
switch (originalName) {
case 'fontFace':
processFontFaceOrdering(ruleContext, node as TSESTree.CallExpression, 0);
break;
// Handle recipe function
if (node.callee.name === 'recipe') {
switch (orderingStrategy) {
case 'alphabetical':
enforceAlphabeticalCSSOrderInRecipe(node as TSESTree.CallExpression, ruleContext);
break;
case 'concentric':
enforceConcentricCSSOrderInRecipe(ruleContext, node as TSESTree.CallExpression);
break;
case 'userDefinedGroupOrder':
if (userDefinedGroupOrder) {
enforceUserDefinedGroupOrderInRecipe(
ruleContext,
node as TSESTree.CallExpression,
userDefinedGroupOrder,
sortRemainingProperties,
);
}
break;
}
case 'globalFontFace':
processFontFaceOrdering(ruleContext, node as TSESTree.CallExpression, 1);
break;
case 'style':
case 'styleVariants':
case 'keyframes':
// Check if this is a wrapper function
const wrapperInfo = tracker.getWrapperInfo(functionName);
const argumentIndex = wrapperInfo?.parameterMapping ?? 0;
processStyleOrdering(
ruleContext,
node as TSESTree.CallExpression,
orderingStrategy,
userDefinedGroupOrder,
sortRemainingProperties,
argumentIndex,
);
break;
case 'globalStyle':
case 'globalKeyframes':
processStyleOrdering(
ruleContext,
node as TSESTree.CallExpression,
orderingStrategy,
userDefinedGroupOrder,
sortRemainingProperties,
1,
);
break;
case 'recipe':
processRecipeOrdering(
ruleContext,
node as TSESTree.CallExpression,
orderingStrategy,
userDefinedGroupOrder,
sortRemainingProperties,
);
break;
}
},
};
};
/**
* Helper function to process style ordering for style-related functions
*/
const processStyleOrdering = (
ruleContext: Rule.RuleContext,
node: TSESTree.CallExpression,
orderingStrategy: OrderingStrategy,
userDefinedGroupOrder?: string[],
sortRemainingProperties?: SortRemainingProperties,
argumentIndex: number = 0,
) => {
if (node.arguments.length > argumentIndex) {
const processProperty = (() => {
switch (orderingStrategy) {
case 'alphabetical':
return enforceAlphabeticalCSSOrderInStyleObject;
case 'concentric':
return enforceConcentricCSSOrderInStyleObject;
case 'userDefinedGroupOrder':
if (!userDefinedGroupOrder || userDefinedGroupOrder.length === 0) {
return enforceAlphabeticalCSSOrderInStyleObject;
}
return (ruleContext: Rule.RuleContext, node: TSESTree.ObjectExpression) =>
enforceUserDefinedGroupOrderInStyleObject(
ruleContext,
node,
userDefinedGroupOrder,
sortRemainingProperties,
);
default:
return enforceAlphabeticalCSSOrderInStyleObject;
}
})();
processStyleNode(ruleContext, node.arguments[argumentIndex] as TSESTree.ObjectExpression, processProperty);
}
};
/**
* Helper function to process font face ordering
*/
const processFontFaceOrdering = (
ruleContext: Rule.RuleContext,
node: TSESTree.CallExpression,
argumentIndex: number,
) => {
if (node.arguments.length > argumentIndex) {
enforceFontFaceOrder(ruleContext, node.arguments[argumentIndex] as TSESTree.ObjectExpression);
}
};
/**
* Helper function to process recipe ordering
*/
const processRecipeOrdering = (
ruleContext: Rule.RuleContext,
node: TSESTree.CallExpression,
orderingStrategy: OrderingStrategy,
userDefinedGroupOrder?: string[],
sortRemainingProperties?: SortRemainingProperties,
) => {
if (node.arguments.length > 0) {
switch (orderingStrategy) {
case 'alphabetical':
enforceAlphabeticalCSSOrderInRecipe(node, ruleContext);
break;
case 'concentric':
enforceConcentricCSSOrderInRecipe(ruleContext, node);
break;
case 'userDefinedGroupOrder':
if (userDefinedGroupOrder) {
enforceUserDefinedGroupOrderInRecipe(ruleContext, node, userDefinedGroupOrder, sortRemainingProperties);
}
break;
}
}
};