Skip to content

Commit 8fdfc28

Browse files
committed
feat(no-physical-properties): detect textAlign physical values like "left"/"right"
This commit adds detection for physical text alignment values ("left"/"right") in the no-physical-properties rule and suggests using logical values ("start"/"end") instead for better RTL language support. - Add physicalPropertyValues mapping to track property-value pairs - Implement extractStringLiteralValue utility function to handle both regular literals and JSX expression containers - Extend test coverage to include textAlign cases with both literal types
1 parent d3ef4a4 commit 8fdfc28

File tree

3 files changed

+173
-2
lines changed

3 files changed

+173
-2
lines changed

plugin/src/rules/no-physical-properties.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { isRecipeVariant, isPandaAttribute, isPandaProp, resolveLonghand } from '../utils/helpers'
22
import { type Rule, createRule } from '../utils'
3-
import { isIdentifier, isJSXIdentifier } from '../utils/nodes'
4-
import { physicalProperties } from '../utils/physical-properties'
3+
import { isIdentifier, isJSXIdentifier, isLiteral, isJSXExpressionContainer } from '../utils/nodes'
4+
import { physicalProperties, physicalPropertyValues } from '../utils/physical-properties'
55
import type { TSESTree } from '@typescript-eslint/utils'
66

77
export const RULE_NAME = 'no-physical-properties'
@@ -15,6 +15,7 @@ const rule: Rule = createRule({
1515
},
1616
messages: {
1717
physical: 'Use logical property instead of {{physical}}. Prefer `{{logical}}`.',
18+
physicalValue: 'Use logical value instead of {{physical}}. Prefer `{{logical}}`.',
1819
replace: 'Replace `{{physical}}` with `{{logical}}`.',
1920
},
2021
type: 'suggestion',
@@ -52,6 +53,32 @@ const rule: Rule = createRule({
5253
const pandaAttributeCache = new WeakMap<TSESTree.Property, boolean | undefined>()
5354
const recipeVariantCache = new WeakMap<TSESTree.Property, boolean | undefined>()
5455

56+
/**
57+
* Extract string literal value from node
58+
* @param valueNode The value node
59+
* @returns String literal value, or null if not found
60+
*/
61+
const extractStringLiteralValue = (
62+
valueNode: TSESTree.Property['value'] | TSESTree.JSXAttribute['value'],
63+
): string | null => {
64+
// Regular literal value (e.g., "left")
65+
if (isLiteral(valueNode) && typeof valueNode.value === 'string') {
66+
return valueNode.value
67+
}
68+
69+
// Literal value in JSX expression container (e.g., {"left"})
70+
if (
71+
isJSXExpressionContainer(valueNode) &&
72+
isLiteral(valueNode.expression) &&
73+
typeof valueNode.expression.value === 'string'
74+
) {
75+
return valueNode.expression.value
76+
}
77+
78+
// Not a string literal
79+
return null
80+
}
81+
5582
const getLonghand = (name: string): string => {
5683
if (longhandCache.has(name)) {
5784
return longhandCache.get(name)!
@@ -118,20 +145,83 @@ const rule: Rule = createRule({
118145
})
119146
}
120147

148+
// Check property values for physical values that should use logical values
149+
const checkPropertyValue = (
150+
keyNode: TSESTree.Identifier | TSESTree.JSXIdentifier,
151+
valueNode: NonNullable<TSESTree.Property['value'] | TSESTree.JSXAttribute['value']>,
152+
) => {
153+
// Skip if property name doesn't have physical values mapping
154+
const propName = keyNode.name
155+
if (!(propName in physicalPropertyValues)) return false
156+
157+
// Extract string literal value
158+
const valueText = extractStringLiteralValue(valueNode)
159+
if (valueText === null) {
160+
// Skip if not a string literal
161+
return false
162+
}
163+
164+
// Check if value is a physical value
165+
const valueMap = physicalPropertyValues[propName]
166+
if (!valueMap[valueText]) return false
167+
168+
const logical = valueMap[valueText]
169+
170+
context.report({
171+
node: valueNode,
172+
messageId: 'physicalValue',
173+
data: {
174+
physical: `"${valueText}"`,
175+
logical: `"${logical}"`,
176+
},
177+
suggest: [
178+
{
179+
messageId: 'replace',
180+
data: {
181+
physical: `"${valueText}"`,
182+
logical: `"${logical}"`,
183+
},
184+
fix: (fixer) => {
185+
if (isLiteral(valueNode)) {
186+
return fixer.replaceText(valueNode, `"${logical}"`)
187+
} else if (isJSXExpressionContainer(valueNode) && isLiteral(valueNode.expression)) {
188+
return fixer.replaceText(valueNode.expression, `"${logical}"`)
189+
}
190+
return null
191+
},
192+
},
193+
],
194+
})
195+
196+
return true
197+
}
198+
121199
return {
122200
JSXAttribute(node: TSESTree.JSXAttribute) {
123201
if (!isJSXIdentifier(node.name)) return
124202
if (!isCachedPandaProp(node)) return
125203

204+
// Check property name
126205
sendReport(node.name)
206+
207+
// Check property value if needed
208+
if (node.value) {
209+
checkPropertyValue(node.name, node.value)
210+
}
127211
},
128212

129213
Property(node: TSESTree.Property) {
130214
if (!isIdentifier(node.key)) return
131215
if (!isCachedPandaAttribute(node)) return
132216
if (isCachedRecipeVariant(node)) return
133217

218+
// Check property name
134219
sendReport(node.key)
220+
221+
// Check property value if needed
222+
if (node.value) {
223+
checkPropertyValue(node.key, node.value)
224+
}
135225
},
136226
}
137227
},

plugin/src/utils/physical-properties.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,12 @@ export const physicalProperties: Record<string, string> = {
3232
top: 'insetBlockStart',
3333
bottom: 'insetBlockEnd',
3434
}
35+
36+
// Map of property names to their physical values and corresponding logical values
37+
export const physicalPropertyValues: Record<string, Record<string, string>> = {
38+
// text-align physical values mapped to logical values
39+
textAlign: {
40+
left: 'start',
41+
right: 'end',
42+
},
43+
}

plugin/tests/no-physical-proeprties.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,42 @@ import { Circle } from './panda/jsx';
2626
2727
function App(){
2828
return <Circle _hover={{ borderBlockEnd: 'solid 1px' }} />;
29+
}`,
30+
},
31+
32+
// textAlign with non-physical values - regular object literal
33+
{
34+
code: javascript`
35+
import { css } from './panda/css';
36+
37+
const styles = css({ textAlign: 'start' })`,
38+
},
39+
40+
{
41+
code: javascript`
42+
import { css } from './panda/css';
43+
44+
function App(){
45+
return <div className={css({ textAlign: 'end' })} />;
46+
}`,
47+
},
48+
49+
// textAlign with non-physical values - JSX expression container
50+
{
51+
code: javascript`
52+
import { Box } from './panda/jsx';
53+
54+
function App(){
55+
return <Box textAlign={"start"} />;
56+
}`,
57+
},
58+
59+
{
60+
code: javascript`
61+
import { Box } from './panda/jsx';
62+
63+
function App(){
64+
return <Box textAlign={"end"} />;
2965
}`,
3066
},
3167
]
@@ -53,6 +89,42 @@ import { Circle } from './panda/jsx';
5389
5490
function App(){
5591
return <Circle _hover={{ borderBottom: 'solid 1px' }} />;
92+
}`,
93+
},
94+
95+
// textAlign with physical values - regular object literal
96+
{
97+
code: javascript`
98+
import { css } from './panda/css';
99+
100+
const styles = css({ textAlign: 'left' })`,
101+
},
102+
103+
{
104+
code: javascript`
105+
import { css } from './panda/css';
106+
107+
function App(){
108+
return <div className={css({ textAlign: 'right' })} />;
109+
}`,
110+
},
111+
112+
// textAlign with physical values - JSX expression container
113+
{
114+
code: javascript`
115+
import { Box } from './panda/jsx';
116+
117+
function App(){
118+
return <Box textAlign={"left"} />;
119+
}`,
120+
},
121+
122+
{
123+
code: javascript`
124+
import { Box } from './panda/jsx';
125+
126+
function App(){
127+
return <Box textAlign={"right"} />;
56128
}`,
57129
},
58130
]

0 commit comments

Comments
 (0)