Skip to content

Commit

Permalink
feat: add sort-object-types rule
Browse files Browse the repository at this point in the history
  • Loading branch information
azat-io committed Jun 3, 2023
1 parent 556690d commit e3a06cf
Show file tree
Hide file tree
Showing 7 changed files with 1,096 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 @@ -258,6 +258,10 @@ export default defineConfig({
text: 'sort-named-imports',
link: '/rules/sort-named-imports',
},
{
text: 'sort-object-types',
link: '/rules/sort-object-types',
},
{
text: 'sort-objects',
link: '/rules/sort-objects',
Expand Down
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ 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-object-types](/rules/sort-object-types) | enforce sorted object types | 🔧 |
| [sort-objects](/rules/sort-objects) | enforce sorted objects | 🔧 |
| [sort-union-types](/rules/sort-union-types) | enforce sorted union types | 🔧 |

Expand Down
146 changes: 146 additions & 0 deletions docs/rules/sort-object-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
title: sort-object-types
---

# sort-object-types

💼 This rule is enabled in the following [configs](/configs/): `recommended-alphabetical`, `recommended-line-length`, `recommended-natural`.

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

## 📖 Rule Details

Enforce sorted object types.

This rule standardizes the order of members of an object type in a TypeScript. The order in which the members are defined within an object type does not affect the type system or the behavior of the code.

## 💡 Examples

### Alphabetical and Natural Sorting

<!-- prettier-ignore -->
```ts
// Incorrect
type User = {
name: string
email: string
role: Role
isAdmin: boolean
}

// Correct
type User = {
email: string
isAdmin: boolean
name: string
role: Role
}
```
### Sorting by Line Length
<!-- prettier-ignore -->
```ts
// Incorrect
type User = {
name: string
email: string
role: Role
isAdmin: boolean
}

// Correct
type User = {
isAdmin: boolean
email: string
name: string
role: Role
}
```
## 🔧 Options
This rule accepts an options object with the following properties:
```ts
interface Options {
type?: 'alphabetical' | 'natural' | 'natural'
order?: 'asc' | 'desc'
'ignore-case'?: boolean
}
```

### type

<sub>(default: `'alphabetical'`)</sub>

- `alphabetical` - sort alphabetically.
- `natural` - sort in natural order.
- `line-length` - sort by code line length.

### order

<sub>(default: `'asc'`)</sub>

- `asc` - enforce properties to be in ascending order.
- `desc` - enforce properties to be in descending order.

### ignore-case

<sub>(default: `false`)</sub>

Only affects alphabetical and natural sorting. When `true` the rule ignores the case-sensitivity of the order.

## ⚙️ Usage

### Legacy Config

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

### Flat Config

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

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

## 🚀 Version

Coming soon.

## 📚 Resources

- [Rule source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/rules/sort-object-types.ts)
- [Test source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/test/sort-object-types.test.ts)
3 changes: 3 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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 sortObjectTypes, { RULE_NAME as sortObjectTypesName } from './rules/sort-object-types'
import sortObjects, { RULE_NAME as sortObjectsName } from './rules/sort-objects'
import sortUnionTypes, { RULE_NAME as sortUnionTypesName } from './rules/sort-union-types'
import { SortType, SortOrder } from './typings'
Expand Down Expand Up @@ -66,6 +67,7 @@ let createConfigWithOptions = (options: {
[sortMapElementsName]: ['error'],
[sortNamedExportsName]: ['error'],
[sortNamedImportsName]: ['error'],
[sortObjectTypesName]: ['error'],
[sortObjectsName]: [
'error',
{
Expand Down Expand Up @@ -96,6 +98,7 @@ export default {
[sortMapElementsName]: sortMapElements,
[sortNamedExportsName]: sortNamedExports,
[sortNamedImportsName]: sortNamedImports,
[sortObjectTypesName]: sortObjectTypes,
[sortObjectsName]: sortObjects,
[sortUnionTypesName]: sortUnionTypes,
},
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export default [perfectionistPluginRecommendedLineLength]
| [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-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-object-types) | enforce sorted object types | 🔧 |
| [sort-objects](https://eslint-plugin-perfectionist.azat.io/rules/sort-objects) | enforce sorted objects | 🔧 |
| [sort-union-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-union-types) | enforce sorted union types | 🔧 |

Expand Down
129 changes: 129 additions & 0 deletions rules/sort-object-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { SortingNode } from '../typings'

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

import { createEslintRule } from '../utils/create-eslint-rule'
import { toSingleLine } from '../utils/to-single-line'
import { rangeToDiff } from '../utils/range-to-diff'
import { SortType, SortOrder } from '../typings'
import { sortNodes } from '../utils/sort-nodes'
import { makeFixes } from '../utils/make-fixes'
import { complete } from '../utils/complete'
import { pairwise } from '../utils/pairwise'
import { compare } from '../utils/compare'

type MESSAGE_ID = 'unexpectedObjectTypesOrder'

type Options = [
Partial<{
'ignore-case': boolean
order: SortOrder
type: SortType
}>,
]

export const RULE_NAME = 'sort-object-types'

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

let source = context.getSourceCode()

let nodes: SortingNode[] = node.members.map(member => {
let name: string

if (member.type === AST_NODE_TYPES.TSPropertySignature) {
if (member.key.type === AST_NODE_TYPES.Identifier) {
;({ name } = member.key)
} else if (member.key.type === AST_NODE_TYPES.Literal) {
name = `${member.key.value}`
} else {
name = source.text.slice(
member.range.at(0),
member.typeAnnotation?.range.at(0),
)
}
} else if (member.type === AST_NODE_TYPES.TSIndexSignature) {
let endIndex: number =
member.typeAnnotation?.range.at(0) ?? member.range.at(1)!

name = source.text.slice(member.range.at(0), endIndex)
} else {
name = source.text.slice(member.range.at(0), member.range.at(1))
}

return {
size: rangeToDiff(member.range),
node: member,
name,
}
})

pairwise(nodes, (first, second) => {
if (compare(first, second, options)) {
context.report({
messageId: 'unexpectedObjectTypesOrder',
data: {
first: toSingleLine(first.name),
second: toSingleLine(second.name),
},
node: second.node,
fix: fixer =>
makeFixes(fixer, nodes, sortNodes(nodes, options), source),
})
}
})
}
},
}),
})
Loading

0 comments on commit e3a06cf

Please sign in to comment.