diff --git a/.changeset/strong-penguins-search.md b/.changeset/strong-penguins-search.md new file mode 100644 index 0000000..69b4cd5 --- /dev/null +++ b/.changeset/strong-penguins-search.md @@ -0,0 +1,5 @@ +--- +"@pandacss/eslint-plugin": patch +--- + +Add `no-physical-properties` rule diff --git a/README.md b/README.md index 46db660..48dbe0f 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Where rules are included in the configs `recommended`, or `all` it is indicated | [`@pandacss/no-invalid-token-paths`](docs/rules/no-invalid-token-paths.md) | ✔️ | | [`@pandacss/no-invalid-nesting`](docs/rules/no-invalid-nesting.md) | ✔️ | | [`@pandacss/no-property-renaming`](docs/rules/no-property-renaming.md) | ✔️ | +| [`@pandacss/no-physical-properties`](docs/rules/no-physical-properties.md) | | | [`@pandacss/no-unsafe-token-fn-usage`](docs/rules/no-unsafe-token-fn-usage.md) | | | [`@pandacss/prefer-longhand-properties`](docs/rules/prefer-longhand-properties.md) | | | [`@pandacss/prefer-shorthand-properties`](docs/rules/prefer-shorthand-properties.md) | | diff --git a/docs/rules/no-physical-properties.md b/docs/rules/no-physical-properties.md new file mode 100644 index 0000000..d94d323 --- /dev/null +++ b/docs/rules/no-physical-properties.md @@ -0,0 +1,60 @@ +[//]: # (This file is generated by eslint-docgen. Do not edit it directly.) + +# no-physical-properties + +Encourage the use of [logical properties](https://mdn.io/logical-properties-basic-concepts) over physical proeprties, to foster a responsive and adaptable user interface. + +📋 This rule is enabled in `plugin:@pandacss/all`. + +## Rule details + +❌ Examples of **incorrect** code: +```js +import { css } from './panda/css'; + +const styles = css({ left: '0' }); +``` +```js + +import { css } from './panda/css'; + +function App(){ + return
; +}; +``` +```js + +import { Circle } from './panda/jsx'; + +function App(){ + return ; +} +``` + +✔️ Examples of **correct** code: +```js +import { css } from './panda/css'; + +const styles = css({ insetInlineStart: '0' }); +``` +```js + +import { css } from './panda/css'; + +function App(){ + return
; +}; +``` +```js + +import { Circle } from './panda/jsx'; + +function App(){ + return ; +} +``` + +## Resources + +* [Rule source](/plugin/src/rules/no-physical-properties.ts) +* [Test source](/tests/no-physical-properties.test.ts) diff --git a/plugin/src/rules/index.ts b/plugin/src/rules/index.ts index 7d0e144..a7088b7 100644 --- a/plugin/src/rules/index.ts +++ b/plugin/src/rules/index.ts @@ -7,6 +7,7 @@ import noHardCodedColor, { RULE_NAME as NoHardCodedColor } from './no-hardcoded- import noImportant, { RULE_NAME as NoImportant } from './no-important' import noInvalidNesting, { RULE_NAME as NoInvalidNesting } from './no-invalid-nesting' import noInvalidTokenPaths, { RULE_NAME as NoInvalidTokenPaths } from './no-invalid-token-paths' +import noPhysicalProperties, { RULE_NAME as NoPhysicalProperties } from './no-physical-properties' import noPropertyRenaming, { RULE_NAME as NoPropertyRenaming } from './no-property-renaming' import noUnsafeTokenUsage, { RULE_NAME as NoUnsafeTokenUsage } from './no-unsafe-token-fn-usage' import preferLonghandProperties, { RULE_NAME as PreferLonghandProperties } from './prefer-longhand-properties' @@ -26,6 +27,7 @@ export const rules = { [NoInvalidTokenPaths]: noInvalidTokenPaths, [NoInvalidNesting]: noInvalidNesting, [NoPropertyRenaming]: noPropertyRenaming, + [NoPhysicalProperties]: noPhysicalProperties, [NoUnsafeTokenUsage]: noUnsafeTokenUsage, [PreferLonghandProperties]: preferLonghandProperties, [PreferShorthandProperties]: preferShorthandProperties, diff --git a/plugin/src/rules/no-physical-properties.ts b/plugin/src/rules/no-physical-properties.ts new file mode 100644 index 0000000..b8d687b --- /dev/null +++ b/plugin/src/rules/no-physical-properties.ts @@ -0,0 +1,75 @@ +import { isPandaAttribute, isPandaProp, resolveLonghand } from '../utils/helpers' +import { type Rule, createRule } from '../utils' +import { isIdentifier, isJSXIdentifier } from '../utils/nodes' +import { physicalProperties } from '../utils/physical-properties' + +export const RULE_NAME = 'no-physical-properties' + +const rule: Rule = createRule({ + name: RULE_NAME, + meta: { + docs: { + description: + 'Encourage the use of [logical properties](https://mdn.io/logical-properties-basic-concepts) over physical proeprties, to foster a responsive and adaptable user interface.', + }, + messages: { + physical: 'Use logical property of {{physical}} instead. Prefer `{{logical}}`', + replace: 'Replace `{{physical}}` with `{{logical}}`', + }, + type: 'suggestion', + hasSuggestions: true, + schema: [], + }, + defaultOptions: [], + create(context) { + const getLonghand = (name: string) => resolveLonghand(name, context) ?? name + + const sendReport = (node: any, name: string) => { + const logical = physicalProperties[getLonghand(name)] + const longhand = resolveLonghand(name, context) + + return context.report({ + node, + messageId: 'physical', + data: { + physical: `\`${name}\`${longhand ? ` - \`${longhand}\`` : ''}`, + logical, + }, + suggest: [ + { + messageId: 'replace', + data: { + physical: name, + logical, + }, + fix: (fixer) => { + return fixer.replaceTextRange(node.range, logical) + }, + }, + ], + }) + } + + return { + JSXAttribute(node) { + if (!isJSXIdentifier(node.name)) return + if (!isPandaProp(node, context)) return + + if (getLonghand(node.name.name) in physicalProperties) { + sendReport(node.name, node.name.name) + } + }, + + Property(node) { + if (!isIdentifier(node.key)) return + if (!isPandaAttribute(node, context)) return + + if (getLonghand(node.key.name) in physicalProperties) { + sendReport(node.key, node.key.name) + } + }, + } + }, +}) + +export default rule diff --git a/plugin/src/rules/prefer-atomic-properties.ts b/plugin/src/rules/prefer-atomic-properties.ts index 1186f09..58d712d 100644 --- a/plugin/src/rules/prefer-atomic-properties.ts +++ b/plugin/src/rules/prefer-atomic-properties.ts @@ -55,6 +55,7 @@ const rule: Rule = createRule({ Property(node) { if (!isIdentifier(node.key)) return if (!isPandaAttribute(node, context)) return + const cmp = resolveCompositeProperty(node.key.name) if (!cmp) return diff --git a/plugin/src/rules/prefer-composite-properties.ts b/plugin/src/rules/prefer-composite-properties.ts index e1fc268..5dc024e 100644 --- a/plugin/src/rules/prefer-composite-properties.ts +++ b/plugin/src/rules/prefer-composite-properties.ts @@ -53,6 +53,7 @@ const rule: Rule = createRule({ Property(node) { if (!isIdentifier(node.key)) return if (!isPandaAttribute(node, context)) return + const atm = resolveCompositeProperty(node.key.name) if (!atm) return diff --git a/plugin/src/rules/prefer-longhand-properties.ts b/plugin/src/rules/prefer-longhand-properties.ts index 4f71fe1..f33b7cf 100644 --- a/plugin/src/rules/prefer-longhand-properties.ts +++ b/plugin/src/rules/prefer-longhand-properties.ts @@ -59,6 +59,7 @@ const rule: Rule = createRule({ Property(node) { if (!isIdentifier(node.key)) return if (!isPandaAttribute(node, context)) return + const longhand = resolveLonghand(node.key.name, context) if (!longhand) return diff --git a/plugin/src/rules/prefer-shorthand-properties.ts b/plugin/src/rules/prefer-shorthand-properties.ts index c85f8be..97a8f59 100644 --- a/plugin/src/rules/prefer-shorthand-properties.ts +++ b/plugin/src/rules/prefer-shorthand-properties.ts @@ -62,6 +62,7 @@ const rule: Rule = createRule({ Property(node) { if (!isIdentifier(node.key)) return if (!isPandaAttribute(node, context)) return + const longhand = resolveLonghand(node.key.name, context) if (longhand) return diff --git a/plugin/src/rules/prefer-unified-property-style.ts b/plugin/src/rules/prefer-unified-property-style.ts index 46b90ce..cca0060 100644 --- a/plugin/src/rules/prefer-unified-property-style.ts +++ b/plugin/src/rules/prefer-unified-property-style.ts @@ -24,10 +24,10 @@ const rule: Rule = createRule({ const getLonghand = (name: string) => resolveLonghand(name, context) ?? name const resolveCompositeProperty = (name: string) => { - if (Object.hasOwn(compositeProperties, name)) return name + if (name in compositeProperties) return name const longhand = getLonghand(name) - if (isValidProperty(longhand, context) && Object.hasOwn(compositeProperties, longhand)) return longhand + if (isValidProperty(longhand, context) && longhand in compositeProperties) return longhand } const sendReport = (node: any, cmp: string, siblings: string[]) => { @@ -54,6 +54,7 @@ const rule: Rule = createRule({ 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 @@ -65,6 +66,7 @@ const rule: Rule = createRule({ 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 diff --git a/plugin/src/utils/physical-properties.ts b/plugin/src/utils/physical-properties.ts new file mode 100644 index 0000000..f45052b --- /dev/null +++ b/plugin/src/utils/physical-properties.ts @@ -0,0 +1,34 @@ +export const physicalProperties: Record = { + borderBottom: 'borderBlockEnd', + borderBottomColor: 'borderBlockEndColor', + borderBottomStyle: 'borderBlockEndStyle', + borderBottomWidth: 'borderBlockEndWidth', + borderTop: 'borderBlockStart', + borderTopColor: 'borderBlockStartColor', + borderTopStyle: 'borderBlockStartStyle', + borderTopWidth: 'borderBlockStartWidth', + borderRight: 'borderInlineEnd', + borderRightColor: 'borderInlineEndColor', + borderRightStyle: 'borderInlineEndStyle', + borderRightWidth: 'borderInlineEndWidth', + borderLeft: 'borderInlineStart', + borderLeftColor: 'borderInlineStartColor', + borderLeftStyle: 'borderInlineStartStyle', + borderLeftWidth: 'borderInlineStartWidth', + borderTopLeftRadius: 'borderStartStartRadius', + borderBottomLeftRadius: 'borderEndStartRadius', + borderTopRightRadius: 'borderStartEndRadius', + borderBottomRightRadius: 'borderEndEndRadius', + marginBottom: 'marginBlockEnd', + marginTop: 'marginBlockStart', + marginRight: 'marginInlineEnd', + marginLeft: 'marginInlineStart', + paddingBottom: 'paddingBlockEnd', + paddingTop: 'paddingBlockStart', + paddingRight: 'paddingInlineEnd', + paddingLeft: 'paddingInlineStart', + left: 'insetInlineStart', + right: 'insetInlineEnd', + top: 'insetBlockStart', + bottom: 'insetBlockEnd', +} diff --git a/plugin/tests/no-physical-proeprties.test.ts b/plugin/tests/no-physical-proeprties.test.ts new file mode 100644 index 0000000..1a256ec --- /dev/null +++ b/plugin/tests/no-physical-proeprties.test.ts @@ -0,0 +1,68 @@ +import { tester } from '../test-utils' +import rule, { RULE_NAME } from '../src/rules/no-physical-properties' + +const javascript = String.raw + +const valids = [ + { + code: javascript` +import { css } from './panda/css'; + +const styles = css({ insetInlineStart: '0' })`, + }, + + { + code: javascript` +import { css } from './panda/css'; + +function App(){ + return
; +}`, + }, + + { + code: javascript` +import { Circle } from './panda/jsx'; + +function App(){ + return ; +}`, + }, +] + +const invalids = [ + { + code: javascript` +import { css } from './panda/css'; + +const styles = css({ left: '0' })`, + }, + + { + code: javascript` +import { css } from './panda/css'; + +function App(){ + return
; +}`, + }, + + { + code: javascript` +import { Circle } from './panda/jsx'; + +function App(){ + 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 3431690..ade9808 100644 --- a/sandbox/src/App.tsx +++ b/sandbox/src/App.tsx @@ -32,7 +32,7 @@ function App() { fontSize: 'token(fontSizes.2xl, 4px)', marginTop: '{spacings.4} token(spacing.600)', margin: '4', - paddingTop: token('sizes.4'), + pt: token('sizes.4'), }) const color = 'red'