eslint-plugin-vanilla-extract/src/css-rules/prefer-logical-properties/logical-properties-visitor-creator.ts
Ante Budimir 5b0bcf17c7 feat 🥁: add prefer-logical-properties rule for i18n-friendly styles
- Add new rule `prefer-logical-properties` that enforces logical CSS properties over physical directional properties
- Detects 140+ physical property mappings across margin, padding, border, inset, size, overflow, and scroll properties
- Supports value-based detection for `text-align`, `float`, `clear`, and `resize` properties
- Provides automatic fixes for all detected violations
- Preserves original formatting (camelCase/kebab-case and quote style)
- Configurable allowlist via `allow` option to skip specific properties
- Comprehensive test coverage
2025-11-09 20:54:11 +02:00

76 lines
3.3 KiB
TypeScript

import type { Rule } from 'eslint';
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
import { processRecipeProperties } from '../shared-utils/recipe-property-processor.js';
import { ReferenceTracker, createReferenceTrackingVisitor } from '../shared-utils/reference-tracker.js';
import { processStyleNode } from '../shared-utils/style-node-processor.js';
import { processLogicalPropertiesInStyleObject, type LogicalPropertiesOptions } from './logical-properties-processor.js';
/**
* Creates ESLint rule visitors for detecting and reporting physical CSS properties
* in vanilla-extract style objects.
*
* - Tracks calls to vanilla-extract APIs (style, recipe, keyframes, globalStyle, fontFace, etc.)
* - Detects physical property names and directional values
* - Respects the `allow` option for allowlisting properties
* - Provides auto-fixes for unambiguous conversions
*
* @param context ESLint rule context used to read options and report diagnostics
* @returns Rule listener that inspects vanilla-extract call expressions and processes style objects
*/
export const createLogicalPropertiesVisitors = (context: Rule.RuleContext): Rule.RuleListener => {
const tracker = new ReferenceTracker();
const trackingVisitor = createReferenceTrackingVisitor(tracker);
const options = (context.options?.[0] as LogicalPropertiesOptions | undefined) || {};
const allowSet = new Set((options.allow ?? []).map((prop) => prop));
const process = (context: Rule.RuleContext, object: TSESTree.ObjectExpression) =>
processLogicalPropertiesInStyleObject(context, object, allowSet);
return {
...trackingVisitor,
CallExpression(node) {
if (node.callee.type !== AST_NODE_TYPES.Identifier) return;
const functionName = node.callee.name;
if (!tracker.isTrackedFunction(functionName)) return;
const originalName = tracker.getOriginalName(functionName);
if (!originalName) return;
switch (originalName) {
case 'fontFace':
if (node.arguments.length > 0 && node.arguments[0]?.type === AST_NODE_TYPES.ObjectExpression) {
process(context, node.arguments[0] as TSESTree.ObjectExpression);
}
break;
case 'globalFontFace':
if (node.arguments.length > 1 && node.arguments[1]?.type === AST_NODE_TYPES.ObjectExpression) {
process(context, node.arguments[1] as TSESTree.ObjectExpression);
}
break;
case 'style':
case 'styleVariants':
case 'keyframes':
if (node.arguments.length > 0) {
processStyleNode(context, node.arguments[0] as TSESTree.Node, (context, object) => process(context, object));
}
break;
case 'globalStyle':
case 'globalKeyframes':
if (node.arguments.length >= 2) {
processStyleNode(context, node.arguments[1] as TSESTree.Node, (context, object) => process(context, object));
}
break;
case 'recipe':
if (node.arguments.length > 0 && node.arguments[0]?.type === AST_NODE_TYPES.ObjectExpression) {
processRecipeProperties(context, node.arguments[0] as TSESTree.ObjectExpression, (context, object) =>
process(context, object),
);
}
break;
}
},
};
};