Skip to content

Commit 8d608f9

Browse files
authored
feat(no-physical-properties): detect textAlign physical values like "left"/"right" (#243)
2 parents d3ef4a4 + e023a18 commit 8d608f9

File tree

4 files changed

+261
-83
lines changed

4 files changed

+261
-83
lines changed

Diff for: docs/rules/no-physical-properties.md

+28
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ function App(){
3030
return <Circle _hover={{ borderBottom: 'solid 1px' }} />;
3131
}
3232
```
33+
```js
34+
35+
import { css } from './panda/css';
36+
37+
const styles = css({ textAlign: 'left' });
38+
```
39+
```js
40+
41+
import { Box } from './panda/jsx';
42+
43+
function App(){
44+
return <Box textAlign={"right"} />;
45+
}
46+
```
3347

3448
✔️ Examples of **correct** code:
3549
```js
@@ -53,6 +67,20 @@ function App(){
5367
return <Circle _hover={{ borderBlockEnd: 'solid 1px' }} />;
5468
}
5569
```
70+
```js
71+
72+
import { css } from './panda/css';
73+
74+
const styles = css({ textAlign: 'start' });
75+
```
76+
```js
77+
78+
import { Box } from './panda/jsx';
79+
80+
function App(){
81+
return <Box textAlign={"end"} />;
82+
}
83+
```
5684

5785
## Resources
5886

Diff for: plugin/src/rules/no-physical-properties.ts

+152-83
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,139 @@
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'
5-
import type { TSESTree } from '@typescript-eslint/utils'
3+
import { isIdentifier, isJSXIdentifier, isLiteral, isJSXExpressionContainer } from '../utils/nodes'
4+
import { physicalProperties, physicalPropertyValues } from '../utils/physical-properties'
5+
import type { TSESTree, TSESLint } from '@typescript-eslint/utils'
6+
7+
type CacheMap<K extends object, V> = WeakMap<K, V | undefined>
8+
type ValueNode = TSESTree.Property['value'] | TSESTree.JSXAttribute['value']
9+
type IdentifierNode = TSESTree.Identifier | TSESTree.JSXIdentifier
10+
type RuleContextType = TSESLint.RuleContext<keyof typeof MESSAGES, [{ whitelist: string[] }]>
611

712
export const RULE_NAME = 'no-physical-properties'
813

14+
const MESSAGES = {
15+
physical: 'Use logical property instead of {{physical}}. Prefer `{{logical}}`.',
16+
physicalValue: 'Use logical value instead of {{physical}}. Prefer `{{logical}}`.',
17+
replace: 'Replace `{{physical}}` with `{{logical}}`.',
18+
} as const
19+
20+
class PropertyCache {
21+
private longhandCache = new Map<string, string>()
22+
private pandaPropCache: CacheMap<TSESTree.JSXAttribute, boolean> = new WeakMap()
23+
private pandaAttributeCache: CacheMap<TSESTree.Property, boolean> = new WeakMap()
24+
private recipeVariantCache: CacheMap<TSESTree.Property, boolean> = new WeakMap()
25+
26+
getLonghand(name: string, context: RuleContextType): string {
27+
if (this.longhandCache.has(name)) {
28+
return this.longhandCache.get(name)!
29+
}
30+
const longhand = resolveLonghand(name, context) ?? name
31+
this.longhandCache.set(name, longhand)
32+
return longhand
33+
}
34+
35+
isPandaProp(node: TSESTree.JSXAttribute, context: RuleContextType): boolean {
36+
if (this.pandaPropCache.has(node)) {
37+
return this.pandaPropCache.get(node)!
38+
}
39+
const result = isPandaProp(node, context)
40+
this.pandaPropCache.set(node, result)
41+
return !!result
42+
}
43+
44+
isPandaAttribute(node: TSESTree.Property, context: RuleContextType): boolean {
45+
if (this.pandaAttributeCache.has(node)) {
46+
return this.pandaAttributeCache.get(node)!
47+
}
48+
const result = isPandaAttribute(node, context)
49+
this.pandaAttributeCache.set(node, result)
50+
return !!result
51+
}
52+
53+
isRecipeVariant(node: TSESTree.Property, context: RuleContextType): boolean {
54+
if (this.recipeVariantCache.has(node)) {
55+
return this.recipeVariantCache.get(node)!
56+
}
57+
const result = isRecipeVariant(node, context)
58+
this.recipeVariantCache.set(node, result)
59+
return !!result
60+
}
61+
}
62+
63+
const extractStringLiteralValue = (valueNode: ValueNode): string | null => {
64+
if (isLiteral(valueNode) && typeof valueNode.value === 'string') {
65+
return valueNode.value
66+
}
67+
68+
if (
69+
isJSXExpressionContainer(valueNode) &&
70+
isLiteral(valueNode.expression) &&
71+
typeof valueNode.expression.value === 'string'
72+
) {
73+
return valueNode.expression.value
74+
}
75+
76+
return null
77+
}
78+
79+
const createPropertyReport = (
80+
node: IdentifierNode,
81+
longhandName: string,
82+
logical: string,
83+
context: RuleContextType,
84+
) => {
85+
const physicalName = `\`${node.name}\`${longhandName !== node.name ? ` (resolved to \`${longhandName}\`)` : ''}`
86+
87+
context.report({
88+
node,
89+
messageId: 'physical',
90+
data: { physical: physicalName, logical },
91+
suggest: [
92+
{
93+
messageId: 'replace',
94+
data: { physical: node.name, logical },
95+
fix: (fixer: TSESLint.RuleFixer) => fixer.replaceText(node, logical),
96+
},
97+
],
98+
})
99+
}
100+
101+
const createValueReport = (
102+
valueNode: NonNullable<ValueNode>,
103+
valueText: string,
104+
logical: string,
105+
context: RuleContextType,
106+
) => {
107+
context.report({
108+
node: valueNode,
109+
messageId: 'physicalValue',
110+
data: { physical: `"${valueText}"`, logical: `"${logical}"` },
111+
suggest: [
112+
{
113+
messageId: 'replace',
114+
data: { physical: `"${valueText}"`, logical: `"${logical}"` },
115+
fix: (fixer: TSESLint.RuleFixer) => {
116+
if (isLiteral(valueNode)) {
117+
return fixer.replaceText(valueNode, `"${logical}"`)
118+
}
119+
if (isJSXExpressionContainer(valueNode) && isLiteral(valueNode.expression)) {
120+
return fixer.replaceText(valueNode.expression, `"${logical}"`)
121+
}
122+
return null
123+
},
124+
},
125+
],
126+
})
127+
}
128+
9129
const rule: Rule = createRule({
10130
name: RULE_NAME,
11131
meta: {
12132
docs: {
13133
description:
14134
'Encourage the use of logical properties over physical properties to foster a responsive and adaptable user interface.',
15135
},
16-
messages: {
17-
physical: 'Use logical property instead of {{physical}}. Prefer `{{logical}}`.',
18-
replace: 'Replace `{{physical}}` with `{{logical}}`.',
19-
},
136+
messages: MESSAGES,
20137
type: 'suggestion',
21138
hasSuggestions: true,
22139
schema: [
@@ -36,102 +153,54 @@ const rule: Rule = createRule({
36153
},
37154
],
38155
},
39-
defaultOptions: [
40-
{
41-
whitelist: [],
42-
},
43-
],
156+
defaultOptions: [{ whitelist: [] }],
44157
create(context) {
45158
const whitelist: string[] = context.options[0]?.whitelist ?? []
159+
const cache = new PropertyCache()
46160

47-
// Cache for resolved longhand properties
48-
const longhandCache = new Map<string, string>()
49-
50-
// Cache for helper functions
51-
const pandaPropCache = new WeakMap<TSESTree.JSXAttribute, boolean | undefined>()
52-
const pandaAttributeCache = new WeakMap<TSESTree.Property, boolean | undefined>()
53-
const recipeVariantCache = new WeakMap<TSESTree.Property, boolean | undefined>()
54-
55-
const getLonghand = (name: string): string => {
56-
if (longhandCache.has(name)) {
57-
return longhandCache.get(name)!
58-
}
59-
const longhand = resolveLonghand(name, context) ?? name
60-
longhandCache.set(name, longhand)
61-
return longhand
62-
}
161+
const checkPropertyName = (node: IdentifierNode) => {
162+
if (whitelist.includes(node.name)) return
163+
const longhandName = cache.getLonghand(node.name, context)
164+
if (!(longhandName in physicalProperties)) return
63165

64-
const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
65-
if (pandaPropCache.has(node)) {
66-
return pandaPropCache.get(node)!
67-
}
68-
const result = isPandaProp(node, context)
69-
pandaPropCache.set(node, result)
70-
return !!result
166+
const logical = physicalProperties[longhandName]
167+
createPropertyReport(node, longhandName, logical, context)
71168
}
72169

73-
const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
74-
if (pandaAttributeCache.has(node)) {
75-
return pandaAttributeCache.get(node)!
76-
}
77-
const result = isPandaAttribute(node, context)
78-
pandaAttributeCache.set(node, result)
79-
return !!result
80-
}
170+
const checkPropertyValue = (keyNode: IdentifierNode, valueNode: NonNullable<ValueNode>): boolean => {
171+
const propName = keyNode.name
172+
if (!(propName in physicalPropertyValues)) return false
81173

82-
const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
83-
if (recipeVariantCache.has(node)) {
84-
return recipeVariantCache.get(node)!
85-
}
86-
const result = isRecipeVariant(node, context)
87-
recipeVariantCache.set(node, result)
88-
return !!result
89-
}
174+
const valueText = extractStringLiteralValue(valueNode)
175+
if (valueText === null) return false
90176

91-
const sendReport = (node: TSESTree.Identifier | TSESTree.JSXIdentifier) => {
92-
if (whitelist.includes(node.name)) return
93-
const longhandName = getLonghand(node.name)
94-
if (!(longhandName in physicalProperties)) return
177+
const valueMap = physicalPropertyValues[propName]
178+
if (!valueMap[valueText]) return false
95179

96-
const logical = physicalProperties[longhandName]
97-
const physicalName = `\`${node.name}\`${longhandName !== node.name ? ` (resolved to \`${longhandName}\`)` : ''}`
98-
99-
context.report({
100-
node,
101-
messageId: 'physical',
102-
data: {
103-
physical: physicalName,
104-
logical,
105-
},
106-
suggest: [
107-
{
108-
messageId: 'replace',
109-
data: {
110-
physical: node.name,
111-
logical,
112-
},
113-
fix: (fixer) => {
114-
return fixer.replaceText(node, logical)
115-
},
116-
},
117-
],
118-
})
180+
createValueReport(valueNode, valueText, valueMap[valueText], context)
181+
return true
119182
}
120183

121184
return {
122185
JSXAttribute(node: TSESTree.JSXAttribute) {
123186
if (!isJSXIdentifier(node.name)) return
124-
if (!isCachedPandaProp(node)) return
187+
if (!cache.isPandaProp(node, context)) return
125188

126-
sendReport(node.name)
189+
checkPropertyName(node.name)
190+
if (node.value) {
191+
checkPropertyValue(node.name, node.value)
192+
}
127193
},
128194

129195
Property(node: TSESTree.Property) {
130196
if (!isIdentifier(node.key)) return
131-
if (!isCachedPandaAttribute(node)) return
132-
if (isCachedRecipeVariant(node)) return
197+
if (!cache.isPandaAttribute(node, context)) return
198+
if (cache.isRecipeVariant(node, context)) return
133199

134-
sendReport(node.key)
200+
checkPropertyName(node.key)
201+
if (node.value) {
202+
checkPropertyValue(node.key, node.value)
203+
}
135204
},
136205
}
137206
},

Diff for: plugin/src/utils/physical-properties.ts

+9
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+
}

0 commit comments

Comments
 (0)