Skip to content

Commit 084f07b

Browse files
committed
feat: add prefer-composite-properties rule
1 parent e523aae commit 084f07b

File tree

9 files changed

+149
-3
lines changed

9 files changed

+149
-3
lines changed

.changeset/shy-geese-fold.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pandacss/eslint-plugin": patch
3+
---
4+
5+
Add `prefer-composite-properties` rule

plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import preferLonghandProperties, { RULE_NAME as PreferLonghandProperties } from
1010
import preferShorthandProperties, { RULE_NAME as PreferShorthandProperties } from './prefer-shorthand-properties'
1111
import noUnsafeTokenUsage, { RULE_NAME as NoUnsafeTokenUsage } from './no-unsafe-token-fn-usage'
1212
import preferAtomicProperties, { RULE_NAME as PreferAtomicProperties } from './prefer-atomic-properties'
13+
import preferCompositeProperties, { RULE_NAME as PreferCompositeProperties } from './prefer-composite-properties'
1314

1415
export const rules = {
1516
[FileNotIncluded]: fileNotIncluded,
@@ -24,4 +25,5 @@ export const rules = {
2425
[PreferShorthandProperties]: preferShorthandProperties,
2526
[NoUnsafeTokenUsage]: noUnsafeTokenUsage,
2627
[PreferAtomicProperties]: preferAtomicProperties,
28+
[PreferCompositeProperties]: preferCompositeProperties,
2729
} as any

plugin/src/rules/no-dynamic-styling.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const rule: Rule = createRule({
4040
return
4141

4242
// Don't warn for objects. Those are conditions
43-
if (isObjectExpression(node.value)) return
43+
if (isObjectExpression(node.value.expression)) return
4444

4545
if (!isPandaProp(node, context)) return
4646

plugin/src/rules/prefer-atomic-properties.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const rule: Rule = createRule({
99
name: RULE_NAME,
1010
meta: {
1111
docs: {
12-
description: 'Encourage the use of atomic properties instead of composite shorthand properties in the codebase.',
12+
description: 'Encourage the use of atomic properties instead of composite properties in the codebase.',
1313
},
1414
messages: {
1515
atomic: 'Use atomic properties of `{{composite}}` instead. Prefer: \n{{atomics}}',
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { isPandaAttribute, isPandaProp, isValidProperty, resolveLonghand } from '../utils/helpers'
2+
import { type Rule, createRule } from '../utils'
3+
import { compositeProperties } from '../utils/composite-properties'
4+
import { isIdentifier, isJSXIdentifier } from '../utils/nodes'
5+
6+
export const RULE_NAME = 'prefer-composite-properties'
7+
8+
const rule: Rule = createRule({
9+
name: RULE_NAME,
10+
meta: {
11+
docs: {
12+
description: 'Encourage the use of composite properties instead of atomic properties in the codebase.',
13+
},
14+
messages: {
15+
composite: 'Use composite property of `{{atomic}}` instead. \nPrefer: {{composite}}',
16+
},
17+
type: 'suggestion',
18+
schema: [],
19+
},
20+
defaultOptions: [],
21+
create(context) {
22+
const resolveCompositeProperty = (name: string) => {
23+
const longhand = resolveLonghand(name, context) ?? name
24+
25+
if (!isValidProperty(longhand, context)) return
26+
return Object.keys(compositeProperties).find((cpd) => compositeProperties[cpd].includes(longhand))
27+
}
28+
29+
const sendReport = (node: any, name: string) => {
30+
const cmp = resolveCompositeProperty(name)!
31+
32+
return context.report({
33+
node,
34+
messageId: 'composite',
35+
data: {
36+
composite: cmp,
37+
atomic: name,
38+
},
39+
})
40+
}
41+
42+
return {
43+
JSXAttribute(node) {
44+
if (!isJSXIdentifier(node.name)) return
45+
if (!isPandaProp(node, context)) return
46+
47+
const atm = resolveCompositeProperty(node.name.name)
48+
if (!atm) return
49+
50+
sendReport(node, node.name.name)
51+
},
52+
53+
Property(node) {
54+
if (!isIdentifier(node.key)) return
55+
if (!isPandaAttribute(node, context)) return
56+
const atm = resolveCompositeProperty(node.key.name)
57+
if (!atm) return
58+
59+
sendReport(node.key, node.key.name)
60+
},
61+
}
62+
},
63+
})
64+
65+
export default rule

plugin/tests/_parsing.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const valids2 = [
7474
'<Circle debug={true} />',
7575
'<Circle color={"red"} />',
7676
'<Circle color={`red`} />',
77+
'<Circle _hover={{ bg: "red.100" }} />',
7778
]
7879

7980
const invalids2 = ['const styles = css({ bg: color })', '<Circle debug={bool} />', '<styled.div color={color} />']
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { tester } from '../test-utils'
2+
import rule, { RULE_NAME } from '../src/rules/prefer-composite-properties'
3+
4+
const javascript = String.raw
5+
6+
const valids = [
7+
{
8+
code: javascript`
9+
import { css } from './panda/css';
10+
11+
const styles = css({ gap: '4' })`,
12+
},
13+
14+
{
15+
code: javascript`
16+
import { css } from './panda/css';
17+
18+
function App(){
19+
return <div className={css({ background: 'red.100' })} />;
20+
}`,
21+
},
22+
23+
{
24+
code: javascript`
25+
import { Circle } from './panda/jsx';
26+
27+
function App(){
28+
return <Circle _hover={{ borderTop: 'solid 1px blue' }} />;
29+
}`,
30+
},
31+
]
32+
33+
const invalids = [
34+
{
35+
code: javascript`
36+
import { css } from './panda/css';
37+
38+
const styles = css({ rowGap: '4', columnGap: '4' })`,
39+
errors: 2,
40+
},
41+
42+
{
43+
code: javascript`
44+
import { css } from './panda/css';
45+
46+
function App(){
47+
return <div className={css({ bgColor: 'red.100' })} />;
48+
}`,
49+
},
50+
51+
{
52+
code: javascript`
53+
import { Circle } from './panda/jsx';
54+
55+
function App(){
56+
return <Circle _hover={{ borderTopStyle: 'solid', borderTopWidth: '1px', borderTopColor: 'blue' }} />;
57+
}`,
58+
errors: 3,
59+
},
60+
]
61+
62+
tester.run(RULE_NAME, rule, {
63+
valid: valids.map(({ code }) => ({
64+
code,
65+
})),
66+
invalid: invalids.map(({ code, errors = 1 }) => ({
67+
code,
68+
errors,
69+
})),
70+
})

sandbox/.eslintrc.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@ module.exports = {
1313
plugins: ['react-refresh'],
1414
rules: {
1515
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
16+
17+
//* Panda rules overrides
18+
'@pandacss/prefer-shorthand-properties': 'off',
1619
},
1720
}

sandbox/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function App() {
5757
})}
5858
>
5959
<panda.a href={`mailto:${1}`} />
60-
<Circle size={circleSize} />
60+
<Circle size={circleSize} _hover={{ bg: 'red.200' }} />
6161
<HStack gap="40px" debug>
6262
<div className={className}>Element 1</div>
6363
<panda.div color={color} fontWeight="bold" fontSize="50px" bg="red.200" borderTopColor={'#111'}>

0 commit comments

Comments
 (0)