@@ -2,22 +2,138 @@ import { isRecipeVariant, isPandaAttribute, isPandaProp, resolveLonghand } from
2
2
import { type Rule , createRule } from '../utils'
3
3
import { isIdentifier , isJSXIdentifier , isLiteral , isJSXExpressionContainer } from '../utils/nodes'
4
4
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 [ ] } ] >
6
11
7
12
export const RULE_NAME = 'no-physical-properties'
8
13
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
+
9
129
const rule : Rule = createRule ( {
10
130
name : RULE_NAME ,
11
131
meta : {
12
132
docs : {
13
133
description :
14
134
'Encourage the use of logical properties over physical properties to foster a responsive and adaptable user interface.' ,
15
135
} ,
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 ,
21
137
type : 'suggestion' ,
22
138
hasSuggestions : true ,
23
139
schema : [
@@ -37,188 +153,51 @@ const rule: Rule = createRule({
37
153
} ,
38
154
] ,
39
155
} ,
40
- defaultOptions : [
41
- {
42
- whitelist : [ ] ,
43
- } ,
44
- ] ,
156
+ defaultOptions : [ { whitelist : [ ] } ] ,
45
157
create ( context ) {
46
158
const whitelist : string [ ] = context . options [ 0 ] ?. whitelist ?? [ ]
159
+ const cache = new PropertyCache ( )
47
160
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 ) => {
119
162
if ( whitelist . includes ( node . name ) ) return
120
- const longhandName = getLonghand ( node . name )
163
+ const longhandName = cache . getLonghand ( node . name , context )
121
164
if ( ! ( longhandName in physicalProperties ) ) return
122
165
123
166
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 )
146
168
}
147
169
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 => {
154
171
const propName = keyNode . name
155
172
if ( ! ( propName in physicalPropertyValues ) ) return false
156
173
157
- // Extract string literal value
158
174
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
163
176
164
- // Check if value is a physical value
165
177
const valueMap = physicalPropertyValues [ propName ]
166
178
if ( ! valueMap [ valueText ] ) return false
167
179
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 )
196
181
return true
197
182
}
198
183
199
184
return {
200
185
JSXAttribute ( node : TSESTree . JSXAttribute ) {
201
186
if ( ! isJSXIdentifier ( node . name ) ) return
202
- if ( ! isCachedPandaProp ( node ) ) return
187
+ if ( ! cache . isPandaProp ( node , context ) ) return
203
188
204
- // Check property name
205
- sendReport ( node . name )
206
-
207
- // Check property value if needed
189
+ checkPropertyName ( node . name )
208
190
if ( node . value ) {
209
191
checkPropertyValue ( node . name , node . value )
210
192
}
211
193
} ,
212
194
213
195
Property ( node : TSESTree . Property ) {
214
196
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
220
199
221
- // Check property value if needed
200
+ checkPropertyName ( node . key )
222
201
if ( node . value ) {
223
202
checkPropertyValue ( node . key , node . value )
224
203
}
0 commit comments