Skip to content

Commit e023a18

Browse files
committed
Refactor
1 parent f09e349 commit e023a18

File tree

1 file changed

+135
-156
lines changed

1 file changed

+135
-156
lines changed

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

+135-156
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,138 @@ import { isRecipeVariant, isPandaAttribute, isPandaProp, resolveLonghand } from
22
import { type Rule, createRule } from '../utils'
33
import { isIdentifier, isJSXIdentifier, isLiteral, isJSXExpressionContainer } from '../utils/nodes'
44
import { physicalProperties, physicalPropertyValues } from '../utils/physical-properties'
5-
import type { TSESTree } from '@typescript-eslint/utils'
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-
physicalValue: 'Use logical value instead of {{physical}}. Prefer `{{logical}}`.',
19-
replace: 'Replace `{{physical}}` with `{{logical}}`.',
20-
},
136+
messages: MESSAGES,
21137
type: 'suggestion',
22138
hasSuggestions: true,
23139
schema: [
@@ -37,188 +153,51 @@ const rule: Rule = createRule({
37153
},
38154
],
39155
},
40-
defaultOptions: [
41-
{
42-
whitelist: [],
43-
},
44-
],
156+
defaultOptions: [{ whitelist: [] }],
45157
create(context) {
46158
const whitelist: string[] = context.options[0]?.whitelist ?? []
159+
const cache = new PropertyCache()
47160

48-
// Cache for resolved longhand properties
49-
const longhandCache = new Map<string, string>()
50-
51-
// Cache for helper functions
52-
const pandaPropCache = new WeakMap<TSESTree.JSXAttribute, boolean | undefined>()
53-
const pandaAttributeCache = new WeakMap<TSESTree.Property, boolean | undefined>()
54-
const recipeVariantCache = new WeakMap<TSESTree.Property, boolean | undefined>()
55-
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-
82-
const getLonghand = (name: string): string => {
83-
if (longhandCache.has(name)) {
84-
return longhandCache.get(name)!
85-
}
86-
const longhand = resolveLonghand(name, context) ?? name
87-
longhandCache.set(name, longhand)
88-
return longhand
89-
}
90-
91-
const isCachedPandaProp = (node: TSESTree.JSXAttribute): boolean => {
92-
if (pandaPropCache.has(node)) {
93-
return pandaPropCache.get(node)!
94-
}
95-
const result = isPandaProp(node, context)
96-
pandaPropCache.set(node, result)
97-
return !!result
98-
}
99-
100-
const isCachedPandaAttribute = (node: TSESTree.Property): boolean => {
101-
if (pandaAttributeCache.has(node)) {
102-
return pandaAttributeCache.get(node)!
103-
}
104-
const result = isPandaAttribute(node, context)
105-
pandaAttributeCache.set(node, result)
106-
return !!result
107-
}
108-
109-
const isCachedRecipeVariant = (node: TSESTree.Property): boolean => {
110-
if (recipeVariantCache.has(node)) {
111-
return recipeVariantCache.get(node)!
112-
}
113-
const result = isRecipeVariant(node, context)
114-
recipeVariantCache.set(node, result)
115-
return !!result
116-
}
117-
118-
const sendReport = (node: TSESTree.Identifier | TSESTree.JSXIdentifier) => {
161+
const checkPropertyName = (node: IdentifierNode) => {
119162
if (whitelist.includes(node.name)) return
120-
const longhandName = getLonghand(node.name)
163+
const longhandName = cache.getLonghand(node.name, context)
121164
if (!(longhandName in physicalProperties)) return
122165

123166
const logical = physicalProperties[longhandName]
124-
const physicalName = `\`${node.name}\`${longhandName !== node.name ? ` (resolved to \`${longhandName}\`)` : ''}`
125-
126-
context.report({
127-
node,
128-
messageId: 'physical',
129-
data: {
130-
physical: physicalName,
131-
logical,
132-
},
133-
suggest: [
134-
{
135-
messageId: 'replace',
136-
data: {
137-
physical: node.name,
138-
logical,
139-
},
140-
fix: (fixer) => {
141-
return fixer.replaceText(node, logical)
142-
},
143-
},
144-
],
145-
})
167+
createPropertyReport(node, longhandName, logical, context)
146168
}
147169

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
170+
const checkPropertyValue = (keyNode: IdentifierNode, valueNode: NonNullable<ValueNode>): boolean => {
154171
const propName = keyNode.name
155172
if (!(propName in physicalPropertyValues)) return false
156173

157-
// Extract string literal value
158174
const valueText = extractStringLiteralValue(valueNode)
159-
if (valueText === null) {
160-
// Skip if not a string literal
161-
return false
162-
}
175+
if (valueText === null) return false
163176

164-
// Check if value is a physical value
165177
const valueMap = physicalPropertyValues[propName]
166178
if (!valueMap[valueText]) return false
167179

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-
180+
createValueReport(valueNode, valueText, valueMap[valueText], context)
196181
return true
197182
}
198183

199184
return {
200185
JSXAttribute(node: TSESTree.JSXAttribute) {
201186
if (!isJSXIdentifier(node.name)) return
202-
if (!isCachedPandaProp(node)) return
187+
if (!cache.isPandaProp(node, context)) return
203188

204-
// Check property name
205-
sendReport(node.name)
206-
207-
// Check property value if needed
189+
checkPropertyName(node.name)
208190
if (node.value) {
209191
checkPropertyValue(node.name, node.value)
210192
}
211193
},
212194

213195
Property(node: TSESTree.Property) {
214196
if (!isIdentifier(node.key)) return
215-
if (!isCachedPandaAttribute(node)) return
216-
if (isCachedRecipeVariant(node)) return
217-
218-
// Check property name
219-
sendReport(node.key)
197+
if (!cache.isPandaAttribute(node, context)) return
198+
if (cache.isRecipeVariant(node, context)) return
220199

221-
// Check property value if needed
200+
checkPropertyName(node.key)
222201
if (node.value) {
223202
checkPropertyValue(node.key, node.value)
224203
}

0 commit comments

Comments
 (0)