Skip to content

Commit

Permalink
feat: add sort-jsx-props rule
Browse files Browse the repository at this point in the history
  • Loading branch information
azat-io committed May 8, 2023
1 parent 2165025 commit 656c86b
Show file tree
Hide file tree
Showing 7 changed files with 375 additions and 3 deletions.
4 changes: 4 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ export default defineConfig({
text: 'sort-interfaces',
link: '/rules/sort-interfaces',
},
{
text: 'sort-jsx-props',
link: '/rules/sort-jsx-props',
},
{
text: 'sort-named-imports',
link: '/rules/sort-named-imports',
Expand Down
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ title: Rules
| Name | Description | 💼 | 🛠 |
| :---------------------------------------------- | :---------------------------------- | :-- | :-- |
| [sort-interfaces](/rules/sort-interfaces) | Enforce sorted interface properties || 🔧 |
| [sort-jsx-props](/rules/sort-jsx-props) | Enforce sorted JSX props || 🔧 |
| [sort-named-imports](/rules/sort-named-imports) | Enforce sorted named imports || 🔧 |
83 changes: 83 additions & 0 deletions docs/rules/sort-jsx-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
title: sort-jsx-props
---

# sort-jsx-props

> Enforce sorted JSX props.
## Rule details

This rule verifies that JSX props are sorted sorted in order of string length.

### Incorrect

```tsx
let Container = () => (
<Input
placeholder="Password"
value={password}
full
className="input"
type="password"
name="element"
error={false}
autoFocus
/>
)
```

### Correct

```tsx
let Container = () => (
<Input
placeholder="Password"
className="input"
value={password}
type="password"
name="element"
error={false}
autoFocus
full
/>
)
```

## Options

This rule is not configurable.

## Usage

### Legacy config

```json
// .eslintrc
{
"rules": {
"perfectionist/sort-jsx-props": "error"
}
}
```

### Flat config

```js
// eslint.config.js
import perfectionist from 'eslint-plugin-perfectionist'

export default {
plugins: {
perfectionist,
},
rules: {
'perfectionist/sort-jsx-props': 'error',
},
}
```

## Resources

- [Rule source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/rules/sort-jsx-props.ts)
- [Test source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/test/sort-jsx-props.test.ts)
8 changes: 5 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import sortInterfaces, {
RULE_NAME as sortInterfacesName,
} from '~/rules/sort-interfaces'
import sortInterfaces, { RULE_NAME as sortInterfacesName } from '~/rules/sort-interfaces'
import sortJsxProps, { RULE_NAME as sortJsxPropsName } from '~/rules/sort-jsx-props'
import sortNamedImports, { RULE_NAME as sortNamedImportsName } from '~/rules/sort-named-imports'
import { name } from '~/package.json'

export default {
name,
rules: {
[sortInterfacesName]: sortInterfaces,
[sortJsxPropsName]: sortJsxProps,
[sortNamedImportsName]: sortNamedImports,
},
}
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ npm install --save-dev eslint-plugin-perfectionist
| Name | Description | 💼 | 🛠 |
| :----------------------------------------------------------------------------------------- | :---------------------------------- | :-- | :-- |
| [sort-interfaces](https://eslint-plugin-perfectionist.azat.io/rules/sort-interfaces) | Enforce sorted interface properties || 🔧 |
| [sort-jsx-props](https://eslint-plugin-perfectionist.azat.io/rules/sort-jsx-props) | Enforce sorted JSX props || 🔧 |
| [sort-named-imports](https://eslint-plugin-perfectionist.azat.io/rules/sort-named-imports) | Enforce sorted named imports || 🔧 |

## See also
Expand Down
83 changes: 83 additions & 0 deletions rules/sort-jsx-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type {
JSXSpreadAttribute,
JSXAttribute,
} from '@typescript-eslint/types/dist/generated/ast-spec'

import { AST_NODE_TYPES } from '@typescript-eslint/types'

import { createEslintRule } from '~/utils/create-eslint-rule'
import { rangeToDiff } from '~/utils/range-to-diff'
import { sortNodes } from '~/utils/sort-nodes'
import type { SortingNode } from '~/typings'

type MESSAGE_ID = 'unexpectedJSXPropsOrder'

type Options = []

export const RULE_NAME = 'sort-jsx-props'

export default createEslintRule<Options, MESSAGE_ID>({
name: RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce sorted interface properties',
recommended: false,
},
messages: {
unexpectedJSXPropsOrder: 'Expected "{{second}}" to come before "{{first}}"',
},
fixable: 'code',
schema: [],
},
defaultOptions: [],
create: context => ({
JSXElement: node => {
let parts: JSXAttribute[][] = node.openingElement.attributes.reduce(
(accumulator: JSXAttribute[][], attribute: JSXSpreadAttribute | JSXAttribute) => {
if (attribute.type === 'JSXAttribute') {
accumulator.at(-1)!.push(attribute)
} else {
accumulator.push([])
}
return accumulator
},
[[]],
)

parts.forEach(part => {
let values: SortingNode[] = part.map(attribute => ({
name:
attribute.name.type === AST_NODE_TYPES.JSXNamespacedName
? `${attribute.name.namespace.name}:${attribute.name.name.name}`
: attribute.name.name,
size: rangeToDiff(attribute.range),
node: attribute,
}))

for (let i = 1; i < values.length; i++) {
let firstIndex = i - 1
let secondIndex = i
let first = values.at(firstIndex)!
let second = values.at(secondIndex)!

if (first.size < second.size) {
context.report({
messageId: 'unexpectedJSXPropsOrder',
data: {
first: first.name,
second: second.name,
},
node: second.node,
fix: fixer => {
let sourceCode = context.getSourceCode()
let { text } = sourceCode
return sortNodes(fixer, text, values)
},
})
}
}
})
},
}),
})
Loading

0 comments on commit 656c86b

Please sign in to comment.