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