Skip to content

Commit

Permalink
feat: add sort-object-keys rule
Browse files Browse the repository at this point in the history
  • Loading branch information
azat-io committed May 23, 2023
1 parent da1dc47 commit 6dcb425
Show file tree
Hide file tree
Showing 7 changed files with 876 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ export default defineConfig({
text: 'sort-named-imports',
link: '/rules/sort-named-imports',
},
{
text: 'sort-object-keys',
link: '/rules/sort-object-keys',
},
{
text: 'sort-union-types',
link: '/rules/sort-union-types',
Expand Down
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ title: Rules
| [sort-map-elements](/rules/sort-map-elements) | Enforce sorted Map elements || 🔧 |
| [sort-named-exports](/rules/sort-named-exports) | Enforce sorted named exports || 🔧 |
| [sort-named-imports](/rules/sort-named-imports) | Enforce sorted named imports || 🔧 |
| [sort-named-object-keys](/rules/sort-object-keys) | Enforce sorted object keys || 🔧 |
| [sort-named-union-types](/rules/sort-union-types) | Enforce sorted union types || 🔧 |
119 changes: 119 additions & 0 deletions docs/rules/sort-object-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
title: sort-object-keys
---

# sort-object-keys

> Enforce sorted object keys.
## 💡 Examples

### Natural sorting

<!-- prettier-ignore -->
```ts
// Incorrect
let family = {
dad: 'Loid Forger',
mom: 'Yor Forger',
daughter: 'Anya Forger',
}

// Correct
let family = {
dad: 'Loid Forger',
daughter: 'Anya Forger',
mom: 'Yor Forger',
}
```

### Sorting by line length

<!-- prettier-ignore -->
```ts
// Incorrect
let family = {
dad: 'Loid Forger',
mom: 'Yor Forger',
daughter: 'Anya Forger',
}

// Correct
let family = {
daughter: 'Anya Forger',
dad: 'Loid Forger',
mom: 'Yor Forger',
}
```

## 🔧 Options

### `type`

- `enum` (default: `natural`):
- `natural` - sorting, which is similar to alphabetical order.
- `line-length` - sort by code line length.

### `order`

- `enum` (default: `asc`):
- `asc` - enforce properties to be in ascending order.
- `desc` - enforce properties to be in descending order.

## ⚙️ Usage

:::tip
If you use the `sort-keys` rule, you should disable it, as it may conflict with the current rule.
:::

### Legacy config

```json
// .eslintrc
{
"rules": {
"perfectionist/sort-object-keys": [
"error",
{
"type": "line-length",
"order": "desc",
"spreadLast": true
}
]
}
}
```

### Flat config

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

export default [
{
plugins: {
perfectionist,
},
rules: {
'perfectionist/sort-object-keys': [
'error',
{
type: 'line-length',
order: 'desc',
spreadLast: true,
},
],
},
},
]
```

## 🚀 Version

Coming soon.

## 📚 Resources

- [Rule source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/rules/sort-object-keys.ts)
- [Test source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/test/sort-object-keys.test.ts)
3 changes: 3 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import sortJsxProps, { RULE_NAME as sortJsxPropsName } from '~/rules/sort-jsx-pr
import sortMapElements, { RULE_NAME as sortMapElementsName } from '~/rules/sort-map-elements'
import sortNamedExports, { RULE_NAME as sortNamedExportsName } from '~/rules/sort-named-exports'
import sortNamedImports, { RULE_NAME as sortNamedImportsName } from '~/rules/sort-named-imports'
import sortObjectKeys, { RULE_NAME as sortObjectKeysName } from '~/rules/sort-object-keys'
import sortUnionTypes, { RULE_NAME as sortUnionTypesName } from '~/rules/sort-union-types'
import { SortType, SortOrder } from '~/typings'
import { name } from '~/package.json'
Expand All @@ -22,6 +23,7 @@ let getRulesWithOptions = (options: {
[sortMapElementsName]: ['error'],
[sortNamedExportsName]: ['error'],
[sortNamedImportsName]: ['error'],
[sortObjectKeysName]: ['error'],
[sortUnionTypesName]: ['error'],
}
return Object.fromEntries(
Expand All @@ -41,6 +43,7 @@ export default {
[sortMapElementsName]: sortMapElements,
[sortNamedExportsName]: sortNamedExports,
[sortNamedImportsName]: sortNamedImports,
[sortObjectKeysName]: sortObjectKeys,
[sortUnionTypesName]: sortUnionTypes,
},
configs: {
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ npm install --save-dev eslint-plugin-perfectionist
| [sort-map-elements](https://eslint-plugin-perfectionist.azat.io/rules/sort-map-elements) | Enforce sorted Map elements || 🔧 |
| [sort-named-exports](https://eslint-plugin-perfectionist.azat.io/rules/sort-named-exports) | Enforce sorted named exports || 🔧 |
| [sort-named-imports](https://eslint-plugin-perfectionist.azat.io/rules/sort-named-imports) | Enforce sorted named imports || 🔧 |
| [sort-object-keys](https://eslint-plugin-perfectionist.azat.io/rules/sort-object-keys) | Enforce sorted object keys || 🔧 |
| [sort-union-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-union-types) | Enforce sorted union types || 🔧 |

## See also
Expand Down
123 changes: 123 additions & 0 deletions rules/sort-object-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { TSESTree } from '@typescript-eslint/types'

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

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

type MESSAGE_ID = 'unexpectedObjectKeysOrder'

type Options = [
Partial<{
order: SortOrder
type: SortType
}>,
]

export const RULE_NAME = 'sort-object-keys'

export default createEslintRule<Options, MESSAGE_ID>({
name: RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Enforce sorted object keys',
recommended: false,
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
type: {
enum: [SortType.natural, SortType['line-length']],
default: SortType.natural,
},
order: {
enum: [SortOrder.asc, SortOrder.desc],
default: SortOrder.asc,
},
},
additionalProperties: false,
},
],
messages: {
unexpectedObjectKeysOrder: 'Expected "{{second}}" to come before "{{first}}"',
},
},
defaultOptions: [
{
type: SortType.natural,
order: SortOrder.asc,
},
],
create: context => ({
ObjectExpression: node => {
if (node.properties.length > 1) {
let options = complete(context.options.at(0), {
type: SortType.natural,
order: SortOrder.asc,
})

let source = context.getSourceCode().text

let formatProperties = (props: TSESTree.ObjectLiteralElement[]): SortingNode[][] =>
props.reduce(
(accumulator: SortingNode[][], prop) => {
if (prop.type === AST_NODE_TYPES.SpreadElement) {
accumulator.push([])
return accumulator
}

let name: string

if (prop.key.type === AST_NODE_TYPES.Identifier) {
;({ name } = prop.key)
} else if (prop.key.type === AST_NODE_TYPES.Literal) {
name = `${prop.key.value}`
} else {
name = source.slice(...prop.key.range)
}

let value = {
size: rangeToDiff(prop.range),
node: prop,
name,
}

accumulator.at(-1)!.push(value)

return accumulator
},
[[]],
)

formatProperties(node.properties).forEach(values => {
if (values.length > 1) {
for (let i = 1; i < values.length; i++) {
let first = values.at(i - 1)!
let second = values.at(i)!

if (compare(first, second, options)) {
context.report({
messageId: 'unexpectedObjectKeysOrder',
data: {
first: first.name,
second: second.name,
},
node: second.node,
fix: fixer => sortNodes(fixer, source, values, options),
})
}
}
}
})
}
},
}),
})
Loading

0 comments on commit 6dcb425

Please sign in to comment.