Skip to content

Commit

Permalink
feat: add sort-exports rule
Browse files Browse the repository at this point in the history
  • Loading branch information
azat-io committed Jun 13, 2023
1 parent dbef415 commit a71eeb3
Show file tree
Hide file tree
Showing 6 changed files with 803 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ description: ESLint Plugin Perfectionist list of rules
| [sort-array-includes](/rules/sort-array-includes) | enforce sorted arrays before include method | 🔧 |
| [sort-classes](/rules/sort-classes) | enforce sorted classes | 🔧 |
| [sort-enums](/rules/sort-enums) | enforce sorted TypeScript enums | 🔧 |
| [sort-exports](/rules/sort-exports) | enforce sorted exports | 🔧 |
| [sort-imports](/rules/sort-imports) | enforce sorted imports | 🔧 |
| [sort-interfaces](/rules/sort-interfaces) | enforce sorted interface properties | 🔧 |
| [sort-jsx-props](/rules/sort-jsx-props) | enforce sorted JSX props | 🔧 |
Expand Down
136 changes: 136 additions & 0 deletions docs/rules/sort-exports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
title: sort-exports
description: ESLint Plugin Perfectionist rule which enforce sorted exports
---

# sort-exports

💼 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 exports.

Sorting exports alphabetically or in a consistent order can enhance the readability and maintainability of your code. When exports are sorted, it becomes simpler to identify any missing or incorrect exports.

## 💡 Examples

::: code-group

<!-- prettier-ignore -->
```js [Alphabetical and Natural Sorting]
// ❌ Incorrect
export { readPackageJson } from './read-package-json'
export { loadDotEnv } from './load-dot-env'
export { getGitBranch } from './get-git-branch'

// ✅ Correct
export { getGitBranch } from './get-git-branch'
export { loadDotEnv } from './load-dot-env'
export { readPackageJson } from './read-package-json'
```

<!-- prettier-ignore -->
```js [Sorting by Line Length]
// ❌ Incorrect
export { readPackageJson } from './read-package-json'
export { loadDotEnv } from './load-dot-env'
export { getGitBranch } from './get-git-branch'

// ✅ Correct
export { readPackageJson } from './read-package-json'
export { getGitBranch } from './get-git-branch'
export { loadDotEnv } from './load-dot-env'
```

:::

## 🔧 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-exports": [
"error",
{
"type": "line-length",
"order": "desc"
}
]
}
}
```

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

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

:::

## 🚀 Version

Coming soon.

## 📚 Resources

- [Rule source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/rules/sort-exports.ts)
- [Test source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/test/sort-exports.test.ts)
3 changes: 3 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import sortJsxProps, { RULE_NAME as sortJsxPropsName } from './rules/sort-jsx-pr
import sortImports, { RULE_NAME as sortImportsName } from './rules/sort-imports'
import sortClasses, { RULE_NAME as sortClassesName } from './rules/sort-classes'
import sortObjects, { RULE_NAME as sortObjectsName } from './rules/sort-objects'
import sortExports, { RULE_NAME as sortExportsName } from './rules/sort-exports'
import sortEnums, { RULE_NAME as sortEnumsName } from './rules/sort-enums'
import { SortOrder, SortType } from './typings'
import { name } from './package.json'
Expand Down Expand Up @@ -90,6 +91,7 @@ let createConfigWithOptions = (options: {
[sortMapElementsName]: ['error'],
[sortUnionTypesName]: ['error'],
[sortInterfacesName]: ['error'],
[sortExportsName]: ['error'],
[sortEnumsName]: ['error'],
}
return {
Expand All @@ -109,6 +111,7 @@ export default {
[sortArrayIncludesName]: sortArrayIncludes,
[sortClassesName]: sortClasses,
[sortEnumsName]: sortEnums,
[sortExportsName]: sortExports,
[sortImportsName]: sortImports,
[sortInterfacesName]: sortInterfaces,
[sortJsxPropsName]: sortJsxProps,
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export default [
| [sort-array-includes](https://eslint-plugin-perfectionist.azat.io/rules/sort-array-includes) | enforce sorted arrays before include method | 🔧 |
| [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 | 🔧 |
Expand Down
128 changes: 128 additions & 0 deletions rules/sort-exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type { TSESTree } from '@typescript-eslint/types'

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

import type { SortingNode } from '../typings'

import { createEslintRule } from '../utils/create-eslint-rule'
import { rangeToDiff } from '../utils/range-to-diff'
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 = 'unexpectedExportsOrder'

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

export const RULE_NAME = 'sort-exports'

export default createEslintRule<Options, MESSAGE_ID>({
name: RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'enforce sorted exports',
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: {
unexpectedExportsOrder: 'Expected "{{right}}" to come before "{{left}}"',
},
},
defaultOptions: [
{
type: SortType.alphabetical,
order: SortOrder.asc,
},
],
create: context => {
let options = complete(context.options.at(0), {
type: SortType.alphabetical,
order: SortOrder.asc,
'ignore-case': false,
})

let parts: SortingNode[][] = [[]]

let registerNode = (
node:
| TSESTree.ExportNamedDeclarationWithSource
| TSESTree.ExportAllDeclaration,
) => {
if (
node.type === AST_NODE_TYPES.ExportAllDeclaration &&
node.exported === null
) {
parts.push([])
} else {
parts.at(-1)!.push({
size: rangeToDiff(node.range),
name: node.source.value,
node,
})
}
}

return {
ExportAllDeclaration: registerNode,
ExportNamedDeclaration: node => {
if (node.source !== null) {
registerNode(node)
}
},
'Program:exit': () => {
let source = context.getSourceCode()

parts.forEach(nodes => {
pairwise(nodes, (left, right) => {
if (compare(left, right, options)) {
context.report({
messageId: 'unexpectedExportsOrder',
data: {
left: left.name,
right: right.name,
},
node: right.node,
fix: fixer =>
makeFixes(fixer, nodes, sortNodes(nodes, options), source),
})
}
})
})
},
}
},
})
Loading

0 comments on commit a71eeb3

Please sign in to comment.