1
1
import { isRecipeVariant , isPandaAttribute , isPandaProp , resolveLonghand } from '../utils/helpers'
2
2
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 [ ] } ] >
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
- replace : 'Replace `{{physical}}` with `{{logical}}`.' ,
19
- } ,
136
+ messages : MESSAGES ,
20
137
type : 'suggestion' ,
21
138
hasSuggestions : true ,
22
139
schema : [
@@ -36,102 +153,54 @@ const rule: Rule = createRule({
36
153
} ,
37
154
] ,
38
155
} ,
39
- defaultOptions : [
40
- {
41
- whitelist : [ ] ,
42
- } ,
43
- ] ,
156
+ defaultOptions : [ { whitelist : [ ] } ] ,
44
157
create ( context ) {
45
158
const whitelist : string [ ] = context . options [ 0 ] ?. whitelist ?? [ ]
159
+ const cache = new PropertyCache ( )
46
160
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
63
165
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 )
71
168
}
72
169
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
81
173
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
90
176
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
95
179
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
119
182
}
120
183
121
184
return {
122
185
JSXAttribute ( node : TSESTree . JSXAttribute ) {
123
186
if ( ! isJSXIdentifier ( node . name ) ) return
124
- if ( ! isCachedPandaProp ( node ) ) return
187
+ if ( ! cache . isPandaProp ( node , context ) ) return
125
188
126
- sendReport ( node . name )
189
+ checkPropertyName ( node . name )
190
+ if ( node . value ) {
191
+ checkPropertyValue ( node . name , node . value )
192
+ }
127
193
} ,
128
194
129
195
Property ( node : TSESTree . Property ) {
130
196
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
133
199
134
- sendReport ( node . key )
200
+ checkPropertyName ( node . key )
201
+ if ( node . value ) {
202
+ checkPropertyValue ( node . key , node . value )
203
+ }
135
204
} ,
136
205
}
137
206
} ,
0 commit comments