Skip to content

Commit

Permalink
feat: add prefer-unified-property-style rule
Browse files Browse the repository at this point in the history
  • Loading branch information
anubra266 committed Feb 9, 2024
1 parent a5609c0 commit 54f309e
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/stale-ants-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pandacss/eslint-plugin": patch
---

Add `prefer-unified-property-style` rule
3 changes: 2 additions & 1 deletion plugin/src/configs/recommended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export default {
'@pandacss/no-config-function-in-source': 'error',
'@pandacss/no-debug': 'warn',
'@pandacss/no-dynamic-styling': 'warn',
'@pandacss/no-property-renaming': 'warn',
'@pandacss/no-invalid-token-paths': 'error',
'@pandacss/no-property-renaming': 'warn',
'@pandacss/prefer-unified-property-style': 'warn',
},
}
2 changes: 2 additions & 0 deletions plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import preferShorthandProperties, { RULE_NAME as PreferShorthandProperties } fro
import noUnsafeTokenUsage, { RULE_NAME as NoUnsafeTokenUsage } from './no-unsafe-token-fn-usage'
import preferAtomicProperties, { RULE_NAME as PreferAtomicProperties } from './prefer-atomic-properties'
import preferCompositeProperties, { RULE_NAME as PreferCompositeProperties } from './prefer-composite-properties'
import preferUnifiedPropertyStyle, { RULE_NAME as PreferUnifiedPropertyStyle } from './prefer-unified-property-style'

export const rules = {
[FileNotIncluded]: fileNotIncluded,
Expand All @@ -26,4 +27,5 @@ export const rules = {
[NoUnsafeTokenUsage]: noUnsafeTokenUsage,
[PreferAtomicProperties]: preferAtomicProperties,
[PreferCompositeProperties]: preferCompositeProperties,
[PreferUnifiedPropertyStyle]: preferUnifiedPropertyStyle,
} as any
12 changes: 6 additions & 6 deletions plugin/src/rules/prefer-atomic-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ const rule: Rule = createRule({
}

const sendReport = (node: any, name: string) => {
const cpd = resolveCompositeProperty(name)!
const cmp = resolveCompositeProperty(name)!

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

return context.report({
node,
Expand All @@ -46,17 +46,17 @@ const rule: Rule = createRule({
if (!isJSXIdentifier(node.name)) return
if (!isPandaProp(node, context)) return

const cpd = resolveCompositeProperty(node.name.name)
if (!cpd) return
const cmp = resolveCompositeProperty(node.name.name)
if (!cmp) return

sendReport(node, node.name.name)
},

Property(node) {
if (!isIdentifier(node.key)) return
if (!isPandaAttribute(node, context)) return
const cpd = resolveCompositeProperty(node.key.name)
if (!cpd) return
const cmp = resolveCompositeProperty(node.key.name)
if (!cmp) return

sendReport(node.key, node.key.name)
},
Expand Down
2 changes: 1 addition & 1 deletion plugin/src/rules/prefer-shorthand-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const rule: Rule = createRule({
const shorthands = resolveShorthands(name, context)
if (!shorthands) return

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

const data = {
longhand: name,
Expand Down
79 changes: 79 additions & 0 deletions plugin/src/rules/prefer-unified-property-style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { isPandaAttribute, isPandaProp, isValidProperty, resolveLonghand } from '../utils/helpers'
import { type Rule, createRule } from '../utils'
import { compositeProperties } from '../utils/composite-properties'
import { isIdentifier, isJSXIdentifier, isJSXOpeningElement, isObjectExpression } from '../utils/nodes'

export const RULE_NAME = 'prefer-unified-property-style'

const rule: Rule = createRule({
name: RULE_NAME,
meta: {
docs: {
description:
'Discourage against mixing atomic and composite forms of the same property in a style declaration. Atomic styles give more consistent results',
},
messages: {
unify:
"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",
},
type: 'suggestion',
schema: [],
},
defaultOptions: [],
create(context) {
const getLonghand = (name: string) => resolveLonghand(name, context) ?? name

const resolveCompositeProperty = (name: string) => {
if (Object.hasOwn(compositeProperties, name)) return name

const longhand = getLonghand(name)
if (isValidProperty(longhand, context) && Object.hasOwn(compositeProperties, longhand)) return longhand
}

const sendReport = (node: any, cmp: string, siblings: string[]) => {
const _atomicProperties = siblings
.filter((prop) => compositeProperties[cmp].includes(getLonghand(prop)))
.map((prop) => `\`${prop}\``)
if (!_atomicProperties.length) return

const atomicProperties = _atomicProperties.join(', ') + (_atomicProperties.length === 1 ? ' style' : ' styles')
const atomics = compositeProperties[cmp].map((name) => `\`${name}\``).join(', ')

context.report({
node,
messageId: 'unify',
data: {
composite: cmp,
atomicProperties,
atomics,
},
})
}

return {
JSXAttribute(node) {
if (!isJSXIdentifier(node.name)) return
if (!isPandaProp(node, context)) return
const cmp = resolveCompositeProperty(node.name.name)
if (!cmp) return
if (!isJSXOpeningElement(node.parent)) return

const siblings = node.parent.attributes.map((attr: any) => attr.name.name)
sendReport(node, cmp, siblings)
},

Property(node) {
if (!isIdentifier(node.key)) return
if (!isPandaAttribute(node, context)) return
const cmp = resolveCompositeProperty(node.key.name)
if (!cmp) return
if (!isObjectExpression(node.parent)) return

const siblings = node.parent.properties.map((prop: any) => isIdentifier(prop.key) && prop.key.name)
sendReport(node.key, cmp, siblings)
},
}
},
})

export default rule
2 changes: 2 additions & 0 deletions plugin/src/utils/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ export const isCallExpression = isNodeOfType(AST_NODE_TYPES.CallExpression)
export const isImportDeclaration = isNodeOfType(AST_NODE_TYPES.ImportDeclaration)

export const isImportSpecifier = isNodeOfType(AST_NODE_TYPES.ImportSpecifier)

export const isProperty = isNodeOfType(AST_NODE_TYPES.Property)
52 changes: 52 additions & 0 deletions plugin/tests/prefer-unified-property-style.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { tester } from '../test-utils'
import rule, { RULE_NAME } from '../src/rules/prefer-unified-property-style'

const javascript = String.raw

const valids = [
{
code: javascript`
import { css } from './panda/css';
const styles = css({ borderColor: 'gray.900', borderWidth: '1px' })`,
},

{
code: javascript`
import { Circle } from './panda/jsx';
function App(){
return <Circle marginTop="2" marginRight="3" />;
}`,
},
]

const invalids = [
{
code: javascript`
import { css } from './panda/css';
const color = 'red.100';
const styles = css({ borderRadius:"lg", borderTopRightRadius: "0" })`,
},

{
code: javascript`
import { Circle } from './panda/jsx';
function App(){
const bool = true;
return <Circle border="solid 1px" borderColor="gray.800" />;
}`,
},
]

tester.run(RULE_NAME, rule, {
valid: valids.map(({ code }) => ({
code,
})),
invalid: invalids.map(({ code }) => ({
code,
errors: 1,
})),
})
14 changes: 11 additions & 3 deletions sandbox/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ function App() {
debug: true,
color: '{colors.red.400}',
fontSize: 'token(fontSizes.2xl, 4px)',
marginInline: '{spacings.4} token(spacing.600)',
marginTop: '{spacings.4} token(spacing.600)',
margin: '4',
paddingTop: token('sizes.4'),
})

Expand All @@ -45,8 +46,8 @@ function App() {
debug: true,
padding: '40px',
align: 'stretch',
bg: 'red.300',
color: '#111',
background: 'red',
backgroundColor: color,
content: "['escape hatch']",
textAlign: ta,
Expand All @@ -60,7 +61,14 @@ function App() {
<Circle size={circleSize} _hover={{ bg: 'red.200' }} />
<HStack gap="40px" debug>
<div className={className}>Element 1</div>
<panda.div color={color} fontWeight="bold" fontSize="50px" bg="red.200" borderTopColor={'#111'}>
<panda.div
color={color}
fontWeight="bold"
fontSize="50px"
bg="red.200"
borderColor="red.500"
borderTopColor={'#111'}
>
Element 2
</panda.div>
</HStack>
Expand Down

0 comments on commit 54f309e

Please sign in to comment.