Skip to content

Commit 54f309e

Browse files
committed
feat: add prefer-unified-property-style rule
1 parent a5609c0 commit 54f309e

File tree

9 files changed

+160
-11
lines changed

9 files changed

+160
-11
lines changed

.changeset/stale-ants-tickle.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-unified-property-style` rule

plugin/src/configs/recommended.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export default {
77
'@pandacss/no-config-function-in-source': 'error',
88
'@pandacss/no-debug': 'warn',
99
'@pandacss/no-dynamic-styling': 'warn',
10-
'@pandacss/no-property-renaming': 'warn',
1110
'@pandacss/no-invalid-token-paths': 'error',
11+
'@pandacss/no-property-renaming': 'warn',
12+
'@pandacss/prefer-unified-property-style': 'warn',
1213
},
1314
}

plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import preferShorthandProperties, { RULE_NAME as PreferShorthandProperties } fro
1111
import noUnsafeTokenUsage, { RULE_NAME as NoUnsafeTokenUsage } from './no-unsafe-token-fn-usage'
1212
import preferAtomicProperties, { RULE_NAME as PreferAtomicProperties } from './prefer-atomic-properties'
1313
import preferCompositeProperties, { RULE_NAME as PreferCompositeProperties } from './prefer-composite-properties'
14+
import preferUnifiedPropertyStyle, { RULE_NAME as PreferUnifiedPropertyStyle } from './prefer-unified-property-style'
1415

1516
export const rules = {
1617
[FileNotIncluded]: fileNotIncluded,
@@ -26,4 +27,5 @@ export const rules = {
2627
[NoUnsafeTokenUsage]: noUnsafeTokenUsage,
2728
[PreferAtomicProperties]: preferAtomicProperties,
2829
[PreferCompositeProperties]: preferCompositeProperties,
30+
[PreferUnifiedPropertyStyle]: preferUnifiedPropertyStyle,
2931
} as any

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ const rule: Rule = createRule({
2727
}
2828

2929
const sendReport = (node: any, name: string) => {
30-
const cpd = resolveCompositeProperty(name)!
30+
const cmp = resolveCompositeProperty(name)!
3131

32-
const atomics = compositeProperties[cpd].map((name) => `\`${name}\``).join(',\n')
32+
const atomics = compositeProperties[cmp].map((name) => `\`${name}\``).join(',\n')
3333

3434
return context.report({
3535
node,
@@ -46,17 +46,17 @@ const rule: Rule = createRule({
4646
if (!isJSXIdentifier(node.name)) return
4747
if (!isPandaProp(node, context)) return
4848

49-
const cpd = resolveCompositeProperty(node.name.name)
50-
if (!cpd) return
49+
const cmp = resolveCompositeProperty(node.name.name)
50+
if (!cmp) return
5151

5252
sendReport(node, node.name.name)
5353
},
5454

5555
Property(node) {
5656
if (!isIdentifier(node.key)) return
5757
if (!isPandaAttribute(node, context)) return
58-
const cpd = resolveCompositeProperty(node.key.name)
59-
if (!cpd) return
58+
const cmp = resolveCompositeProperty(node.key.name)
59+
if (!cmp) return
6060

6161
sendReport(node.key, node.key.name)
6262
},

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const rule: Rule = createRule({
2525
const shorthands = resolveShorthands(name, context)
2626
if (!shorthands) return
2727

28-
const shorthand = shorthands.map((s) => '`' + s + '`')?.join(', ')
28+
const shorthand = shorthands.map((s) => `\`${s}\``)?.join(', ')
2929

3030
const data = {
3131
longhand: name,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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, isJSXOpeningElement, isObjectExpression } from '../utils/nodes'
5+
6+
export const RULE_NAME = 'prefer-unified-property-style'
7+
8+
const rule: Rule = createRule({
9+
name: RULE_NAME,
10+
meta: {
11+
docs: {
12+
description:
13+
'Discourage against mixing atomic and composite forms of the same property in a style declaration. Atomic styles give more consistent results',
14+
},
15+
messages: {
16+
unify:
17+
"You're mixing atomic {{atomicProperties}} with composite property {{composite}}. \nPrefer atomic styling to mixing atomic and composite properties. \nRemove `{{composite}}` and use one or more of {{atomics}} instead",
18+
},
19+
type: 'suggestion',
20+
schema: [],
21+
},
22+
defaultOptions: [],
23+
create(context) {
24+
const getLonghand = (name: string) => resolveLonghand(name, context) ?? name
25+
26+
const resolveCompositeProperty = (name: string) => {
27+
if (Object.hasOwn(compositeProperties, name)) return name
28+
29+
const longhand = getLonghand(name)
30+
if (isValidProperty(longhand, context) && Object.hasOwn(compositeProperties, longhand)) return longhand
31+
}
32+
33+
const sendReport = (node: any, cmp: string, siblings: string[]) => {
34+
const _atomicProperties = siblings
35+
.filter((prop) => compositeProperties[cmp].includes(getLonghand(prop)))
36+
.map((prop) => `\`${prop}\``)
37+
if (!_atomicProperties.length) return
38+
39+
const atomicProperties = _atomicProperties.join(', ') + (_atomicProperties.length === 1 ? ' style' : ' styles')
40+
const atomics = compositeProperties[cmp].map((name) => `\`${name}\``).join(', ')
41+
42+
context.report({
43+
node,
44+
messageId: 'unify',
45+
data: {
46+
composite: cmp,
47+
atomicProperties,
48+
atomics,
49+
},
50+
})
51+
}
52+
53+
return {
54+
JSXAttribute(node) {
55+
if (!isJSXIdentifier(node.name)) return
56+
if (!isPandaProp(node, context)) return
57+
const cmp = resolveCompositeProperty(node.name.name)
58+
if (!cmp) return
59+
if (!isJSXOpeningElement(node.parent)) return
60+
61+
const siblings = node.parent.attributes.map((attr: any) => attr.name.name)
62+
sendReport(node, cmp, siblings)
63+
},
64+
65+
Property(node) {
66+
if (!isIdentifier(node.key)) return
67+
if (!isPandaAttribute(node, context)) return
68+
const cmp = resolveCompositeProperty(node.key.name)
69+
if (!cmp) return
70+
if (!isObjectExpression(node.parent)) return
71+
72+
const siblings = node.parent.properties.map((prop: any) => isIdentifier(prop.key) && prop.key.name)
73+
sendReport(node.key, cmp, siblings)
74+
},
75+
}
76+
},
77+
})
78+
79+
export default rule

plugin/src/utils/nodes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ export const isCallExpression = isNodeOfType(AST_NODE_TYPES.CallExpression)
3434
export const isImportDeclaration = isNodeOfType(AST_NODE_TYPES.ImportDeclaration)
3535

3636
export const isImportSpecifier = isNodeOfType(AST_NODE_TYPES.ImportSpecifier)
37+
38+
export const isProperty = isNodeOfType(AST_NODE_TYPES.Property)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { tester } from '../test-utils'
2+
import rule, { RULE_NAME } from '../src/rules/prefer-unified-property-style'
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({ borderColor: 'gray.900', borderWidth: '1px' })`,
12+
},
13+
14+
{
15+
code: javascript`
16+
import { Circle } from './panda/jsx';
17+
18+
function App(){
19+
return <Circle marginTop="2" marginRight="3" />;
20+
}`,
21+
},
22+
]
23+
24+
const invalids = [
25+
{
26+
code: javascript`
27+
import { css } from './panda/css';
28+
29+
const color = 'red.100';
30+
const styles = css({ borderRadius:"lg", borderTopRightRadius: "0" })`,
31+
},
32+
33+
{
34+
code: javascript`
35+
import { Circle } from './panda/jsx';
36+
37+
function App(){
38+
const bool = true;
39+
return <Circle border="solid 1px" borderColor="gray.800" />;
40+
}`,
41+
},
42+
]
43+
44+
tester.run(RULE_NAME, rule, {
45+
valid: valids.map(({ code }) => ({
46+
code,
47+
})),
48+
invalid: invalids.map(({ code }) => ({
49+
code,
50+
errors: 1,
51+
})),
52+
})

sandbox/src/App.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ function App() {
3030
debug: true,
3131
color: '{colors.red.400}',
3232
fontSize: 'token(fontSizes.2xl, 4px)',
33-
marginInline: '{spacings.4} token(spacing.600)',
33+
marginTop: '{spacings.4} token(spacing.600)',
34+
margin: '4',
3435
paddingTop: token('sizes.4'),
3536
})
3637

@@ -45,8 +46,8 @@ function App() {
4546
debug: true,
4647
padding: '40px',
4748
align: 'stretch',
48-
bg: 'red.300',
4949
color: '#111',
50+
background: 'red',
5051
backgroundColor: color,
5152
content: "['escape hatch']",
5253
textAlign: ta,
@@ -60,7 +61,14 @@ function App() {
6061
<Circle size={circleSize} _hover={{ bg: 'red.200' }} />
6162
<HStack gap="40px" debug>
6263
<div className={className}>Element 1</div>
63-
<panda.div color={color} fontWeight="bold" fontSize="50px" bg="red.200" borderTopColor={'#111'}>
64+
<panda.div
65+
color={color}
66+
fontWeight="bold"
67+
fontSize="50px"
68+
bg="red.200"
69+
borderColor="red.500"
70+
borderTopColor={'#111'}
71+
>
6472
Element 2
6573
</panda.div>
6674
</HStack>

0 commit comments

Comments
 (0)