diff --git a/.changeset/stale-ants-tickle.md b/.changeset/stale-ants-tickle.md
new file mode 100644
index 0000000..0f5dd59
--- /dev/null
+++ b/.changeset/stale-ants-tickle.md
@@ -0,0 +1,5 @@
+---
+"@pandacss/eslint-plugin": patch
+---
+
+Add `prefer-unified-property-style` rule
diff --git a/plugin/src/configs/recommended.ts b/plugin/src/configs/recommended.ts
index b29c3e1..c2d160e 100644
--- a/plugin/src/configs/recommended.ts
+++ b/plugin/src/configs/recommended.ts
@@ -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',
},
}
diff --git a/plugin/src/rules/index.ts b/plugin/src/rules/index.ts
index 7dd1e63..dd62951 100644
--- a/plugin/src/rules/index.ts
+++ b/plugin/src/rules/index.ts
@@ -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,
@@ -26,4 +27,5 @@ export const rules = {
[NoUnsafeTokenUsage]: noUnsafeTokenUsage,
[PreferAtomicProperties]: preferAtomicProperties,
[PreferCompositeProperties]: preferCompositeProperties,
+ [PreferUnifiedPropertyStyle]: preferUnifiedPropertyStyle,
} as any
diff --git a/plugin/src/rules/prefer-atomic-properties.ts b/plugin/src/rules/prefer-atomic-properties.ts
index d1ecefe..1186f09 100644
--- a/plugin/src/rules/prefer-atomic-properties.ts
+++ b/plugin/src/rules/prefer-atomic-properties.ts
@@ -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,
@@ -46,8 +46,8 @@ 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)
},
@@ -55,8 +55,8 @@ const rule: Rule = createRule({
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)
},
diff --git a/plugin/src/rules/prefer-shorthand-properties.ts b/plugin/src/rules/prefer-shorthand-properties.ts
index 1db687d..c85f8be 100644
--- a/plugin/src/rules/prefer-shorthand-properties.ts
+++ b/plugin/src/rules/prefer-shorthand-properties.ts
@@ -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,
diff --git a/plugin/src/rules/prefer-unified-property-style.ts b/plugin/src/rules/prefer-unified-property-style.ts
new file mode 100644
index 0000000..46b90ce
--- /dev/null
+++ b/plugin/src/rules/prefer-unified-property-style.ts
@@ -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
diff --git a/plugin/src/utils/nodes.ts b/plugin/src/utils/nodes.ts
index 4cff35e..3cd3998 100644
--- a/plugin/src/utils/nodes.ts
+++ b/plugin/src/utils/nodes.ts
@@ -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)
diff --git a/plugin/tests/prefer-unified-property-style.test.ts b/plugin/tests/prefer-unified-property-style.test.ts
new file mode 100644
index 0000000..c007118
--- /dev/null
+++ b/plugin/tests/prefer-unified-property-style.test.ts
@@ -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 ;
+}`,
+ },
+]
+
+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 ;
+}`,
+ },
+]
+
+tester.run(RULE_NAME, rule, {
+ valid: valids.map(({ code }) => ({
+ code,
+ })),
+ invalid: invalids.map(({ code }) => ({
+ code,
+ errors: 1,
+ })),
+})
diff --git a/sandbox/src/App.tsx b/sandbox/src/App.tsx
index cdb997f..6ee0788 100644
--- a/sandbox/src/App.tsx
+++ b/sandbox/src/App.tsx
@@ -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'),
})
@@ -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,
@@ -60,7 +61,14 @@ function App() {
Element 1
-
+
Element 2