11import { isRecipeVariant , isPandaAttribute , isPandaProp , resolveLonghand } from '../utils/helpers'
22import { 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
712export 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+
9129const 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 } ,
0 commit comments