Skip to content

Commit

Permalink
feat: add sort-intersection-types rule
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg committed Apr 9, 2024
1 parent 89c05f6 commit 3ad40ff
Show file tree
Hide file tree
Showing 5 changed files with 1,061 additions and 18 deletions.
148 changes: 148 additions & 0 deletions docs/rules/sort-intersection-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
title: sort-intersection-types
description: ESLint Plugin Perfectionist rule which enforce sorted intersection types in TypeScript
---

# sort-intersection-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 intersection types in TypeScript.

Adhering to the `sort-intersection-types` rule enables developers to ensure that intersection types are consistently sorted, resulting in cleaner and more maintainable code. This rule promotes a standardized ordering of intersection types, making it easier for developers to navigate and understand the structure of type intersections within the codebase.

:::info Important
If you use the [`sort-type-constituents`](https://typescript-eslint.io/rules/sort-type-constituents) rule from the [`@typescript-eslint/eslint-plugin`](https://typescript-eslint.io) plugin, it is highly recommended to [disable it](https://eslint.org/docs/latest/use/configure/rules#using-configuration-files-1) to avoid conflicts.
:::

## 💡 Examples

::: code-group

<!-- prettier-ignore -->
```ts [Alphabetical and Natural Sorting]
// ❌ Incorrect
type NetworkState =
& Failed
& LoadedFromCache
& Success
& DataLoading

// ✅ Correct
type NetworkState =
& DataLoading
& Failed
& LoadedFromCache
& Success
```
<!-- prettier-ignore -->
```ts [Sorting by Line Length]
// ❌ Incorrect
type NetworkState =
& Failed
& LoadedFromCache
& Success
& DataLoading

// ✅ Correct
type NetworkState =
& LoadedFromCache
& DataLoading
& Success
& Failed
```
:::
## 🔧 Options
This rule accepts an options object with the following properties:
```ts
interface Options {
type?: 'alphabetical' | 'natural' | 'line-length'
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

::: code-group

```json [Legacy Config]
// .eslintrc
{
"plugins": ["perfectionist"],
"rules": {
"perfectionist/sort-intersection-types": [
"error",
{
"type": "natural",
"order": "asc"
}
]
}
}
```

```js [Flat Config]
// eslint.config.js
import perfectionist from 'eslint-plugin-perfectionist'

export default [
{
plugins: {
perfectionist,
},
rules: {
'perfectionist/sort-intersection-types': [
'error',
{
type: 'natural',
order: 'asc',
},
],
},
},
]
```

:::

## 🚀 Version

This rule was introduced in v0.4.0.

## 📚 Resources

- [Rule source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/rules/sort-intersection-types.ts)
- [Test source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/test/sort-intersection-types.test.ts)
3 changes: 3 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sortIntersectionTypes, { RULE_NAME as sortIntersectionTypesName } from './rules/sort-intersection-types'
import sortSvelteAttributes, { RULE_NAME as sortSvelteAttributesName } from './rules/sort-svelte-attributes'
import sortAstroAttributes, { RULE_NAME as sortAstroAttributesName } from './rules/sort-astro-attributes'
import sortArrayIncludes, { RULE_NAME as sortArrayIncludesName } from './rules/sort-array-includes'
Expand Down Expand Up @@ -84,6 +85,7 @@ let createConfigWithOptions = (options: {
'spread-last': true,
},
],
[sortIntersectionTypesName]: ['error'],
[sortSvelteAttributesName]: ['error'],
[sortAstroAttributesName]: ['error'],
[sortVueAttributesName]: ['error'],
Expand Down Expand Up @@ -125,6 +127,7 @@ export default {
[sortObjectTypesName]: sortObjectTypes,
[sortObjectsName]: sortObjects,
[sortSvelteAttributesName]: sortSvelteAttributes,
[sortIntersectionTypesName]: sortIntersectionTypes,
[sortUnionTypesName]: sortUnionTypes,
[sortVueAttributesName]: sortVueAttributes,
},
Expand Down
37 changes: 19 additions & 18 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,24 +138,25 @@ export default [

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

| Name | Description | 🔧 |
| :------------------------------------------------------------------------------------------------- | :------------------------------------------ | :-- |
| [sort-array-includes](https://eslint-plugin-perfectionist.azat.io/rules/sort-array-includes) | enforce sorted arrays before include method | 🔧 |
| [sort-astro-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-astro-attributes) | enforce sorted Astro attributes | 🔧 |
| [sort-classes](https://eslint-plugin-perfectionist.azat.io/rules/sort-classes) | enforce sorted classes | 🔧 |
| [sort-enums](https://eslint-plugin-perfectionist.azat.io/rules/sort-enums) | enforce sorted TypeScript enums | 🔧 |
| [sort-exports](https://eslint-plugin-perfectionist.azat.io/rules/sort-exports) | enforce sorted exports | 🔧 |
| [sort-imports](https://eslint-plugin-perfectionist.azat.io/rules/sort-imports) | enforce sorted imports | 🔧 |
| [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-maps](https://eslint-plugin-perfectionist.azat.io/rules/sort-maps) | 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-svelte-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-svelte-attributes) | enforce sorted Svelte attributes | 🔧 |
| [sort-union-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-union-types) | enforce sorted union types | 🔧 |
| [sort-vue-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-vue-attributes) | enforce sorted Vue attributes | 🔧 |
| Name | Description | 🔧 |
| :--------------------------------------------------------------------------------------------------- | :------------------------------------------ | :-- |
| [sort-array-includes](https://eslint-plugin-perfectionist.azat.io/rules/sort-array-includes) | enforce sorted arrays before include method | 🔧 |
| [sort-astro-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-astro-attributes) | enforce sorted Astro attributes | 🔧 |
| [sort-classes](https://eslint-plugin-perfectionist.azat.io/rules/sort-classes) | enforce sorted classes | 🔧 |
| [sort-enums](https://eslint-plugin-perfectionist.azat.io/rules/sort-enums) | enforce sorted TypeScript enums | 🔧 |
| [sort-exports](https://eslint-plugin-perfectionist.azat.io/rules/sort-exports) | enforce sorted exports | 🔧 |
| [sort-imports](https://eslint-plugin-perfectionist.azat.io/rules/sort-imports) | enforce sorted imports | 🔧 |
| [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-maps](https://eslint-plugin-perfectionist.azat.io/rules/sort-maps) | 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-svelte-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-svelte-attributes) | enforce sorted Svelte attributes | 🔧 |
| [sort-intersection-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-intersection-types) | enforce sorted intersection types | 🔧 |
| [sort-union-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-union-types) | enforce sorted union types | 🔧 |
| [sort-vue-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-vue-attributes) | enforce sorted Vue attributes | 🔧 |

<!-- end auto-generated rules list -->

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

import { createEslintRule } from '../utils/create-eslint-rule'
import { toSingleLine } from '../utils/to-single-line'
import { rangeToDiff } from '../utils/range-to-diff'
import { isPositive } from '../utils/is-positive'
import { SortOrder, SortType } 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 = 'unexpectedIntersectionTypesOrder'

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

export const RULE_NAME = 'sort-intersection-types'

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

let nodes: SortingNode[] = node.types.map(type => ({
group:
type.type === 'TSNullKeyword' || type.type === 'TSUndefinedKeyword'
? 'nullable'
: 'unknown',
name: context.sourceCode.text.slice(...type.range),
size: rangeToDiff(type.range),
node: type,
}))

pairwise(nodes, (left, right) => {
let compareValue = isPositive(compare(left, right, options))

if (compareValue) {
context.report({
messageId: 'unexpectedIntersectionTypesOrder',
data: {
left: toSingleLine(left.name),
right: toSingleLine(right.name),
},
node: right.node,
fix: fixer => {
let sortedNodes = sortNodes(nodes, options)

return makeFixes(fixer, nodes, sortedNodes, context.sourceCode)
},
})
}
})
},
}),
})

0 comments on commit 3ad40ff

Please sign in to comment.