Skip to content

Commit

Permalink
feat: add sort-variable-declarations rule
Browse files Browse the repository at this point in the history
  • Loading branch information
azat-io committed Jul 22, 2024
1 parent d9f0fc7 commit 12bd265
Show file tree
Hide file tree
Showing 6 changed files with 928 additions and 3 deletions.
2 changes: 1 addition & 1 deletion docs/content/rules/sort-switch-case.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ Controls whether sorting should be case-sensitive or not.

## Version

This rule was introduced in [v0.2.0](https://github.com/azat-io/eslint-plugin-perfectionist/releases/tag/v3.0.0).
This rule was introduced in [v3.0.0](https://github.com/azat-io/eslint-plugin-perfectionist/releases/tag/v3.0.0).

## Resources

Expand Down
175 changes: 175 additions & 0 deletions docs/content/rules/sort-variable-declarations.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
---
title: sort-variable-declarations
description: The sort-variable-declarations rule in ESLint enforces a consistent order of variable declarations within a scope, improving code readability and maintainability
shortDescription: Enforce sorted variable declarations
keywords:
- eslint
- eslint rule
- coding standards
- code quality
- javascript linting
- variable declarations
- variable order
- variable sorting
- variable declaration order
- sort vars
- sort variables
- sort constants
---

import CodeExample from '../../components/CodeExample.svelte'
import Important from '../../components/Important.astro'
import CodeTabs from '../../components/CodeTabs.svelte'
import { dedent } from 'ts-dedent'

Enforce sorted variable declarations within a scope.

Variable declarations within a block of code can quickly become disorganized and difficult to navigate, especially in larger functions or modules. With this rule, you can ensure that all variable declarations are consistently sorted, making it easier to locate specific variables and maintain a clean and structured codebase.

This practice improves readability and maintainability by providing a predictable order for variable declarations. It helps developers quickly understand the scope and usage of variables without having to search through an unsorted list.

## Try it out

<CodeExample
alphabetical={dedent`
const API_KEY = 'e7c3b6d4-7b7d-4b3b-8b3b-7b3b7b3b7b3b',
apiUrl = 'https://api.perfectionist.dev',
data = fetchData(),
isAuthenticated = checkAuth(),
user = getCurrentUser()
const config = loadConfig(),
database = connectToDatabase(),
environment = process.env.NODE_ENV,
logger = createLogger(),
server = createServer()
`}
lineLength={dedent`
const API_KEY = 'e7c3b6d4-7b7d-4b3b-8b3b-7b3b7b3b7b3b',
apiUrl = 'https://api.perfectionist.dev',
isAuthenticated = checkAuth(),
user = getCurrentUser(),
data = fetchData()
const environment = process.env.NODE_ENV,
database = connectToDatabase(),
logger = createLogger(),
server = createServer(),
config = loadConfig()
`}
initial={dedent`
const data = fetchData(),
isAuthenticated = checkAuth(),
API_KEY = 'e7c3b6d4-7b7d-4b3b-8b3b-7b3b7b3b7b3b',
user = getCurrentUser(),
apiUrl = 'https://api.perfectionist.dev'
const logger = createLogger(),
database = connectToDatabase(),
config = loadConfig(),
environment = process.env.NODE_ENV,
server = createServer()
`}
client:load
lang="js"
/>

## Options

This rule accepts an options object with the following properties:

### type

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

Specifies the sorting method.

- `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”).
- `'natural'` — Sort items in a natural order (e.g., “item2” < “item10”).
- `'line-length'` — Sort items by the length of the code line (shorter lines first).

### order

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

Determines whether the sorted items should be in ascending or descending order.

- `'asc'` — Sort items in ascending order (A to Z, 1 to 9).
- `'desc'` — Sort items in descending order (Z to A, 9 to 1).

### ignoreCase

<sub>default: `true`</sub>

Controls whether sorting should be case-sensitive or not.

- `true` — Ignore case when sorting alphabetically or naturally (e.g., “A” and “a” are the same).
- `false` — Consider case when sorting (e.g., “A” comes before “a”).

## Usage

<CodeTabs
code={[
{
source: dedent`
// eslint.config.js
import perfectionist from 'eslint-plugin-perfectionist'
export default [
{
plugins: {
perfectionist,
},
rules: {
'perfectionist/sort-variable-declarations': [
'error',
{
type: 'alphabetical',
order: 'asc',
ignoreCase: true,
},
],
},
},
]
`,
name: 'Flat Config',
value: 'flat',
},
{
source: dedent`
// .eslintrc.js
export default {
plugins: [
'perfectionist',
],
rules: {
'perfectionist/sort-variable-declarations': [
'error',
{
type: 'alphabetical',
order: 'asc',
ignoreCase: true,
},
],
},
}
`,
name: 'Legacy Config',
value: 'legacy',
},
]}
type="config-type"
client:load
lang="ts"
/>

## Version

This rule was introduced in [v3.0.0](https://github.com/azat-io/eslint-plugin-perfectionist/releases/tag/v3.0.0).

## Resources

- [Rule source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/rules/sort-variable-declarations.ts)
- [Test source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/test/sort-variable-declarations.test.ts)

2 changes: 2 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
FlatConfig,
} from '@typescript-eslint/utils/ts-eslint'

import sortVariableDeclarations from './rules/sort-variable-declarations'
import sortIntersectionTypes from './rules/sort-intersection-types'
import sortSvelteAttributes from './rules/sort-svelte-attributes'
import sortAstroAttributes from './rules/sort-astro-attributes'
Expand Down Expand Up @@ -35,6 +36,7 @@ let name = 'perfectionist'

let plugin = {
rules: {
'sort-variable-declarations': sortVariableDeclarations,
'sort-intersection-types': sortIntersectionTypes,
'sort-svelte-attributes': sortSvelteAttributes,
'sort-astro-attributes': sortAstroAttributes,
Expand Down
2 changes: 0 additions & 2 deletions rules/sort-array-includes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ type Options = [
}>,
]

export const RULE_NAME = 'sort-array-includes'

export default createEslintRule<Options, MESSAGE_ID>({
name: 'sort-array-includes',
meta: {
Expand Down
166 changes: 166 additions & 0 deletions rules/sort-variable-declarations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import type { TSESTree } from '@typescript-eslint/types'

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

import { createEslintRule } from '../utils/create-eslint-rule'
import { getSourceCode } from '../utils/get-source-code'
import { toSingleLine } from '../utils/to-single-line'
import { rangeToDiff } from '../utils/range-to-diff'
import { isPositive } from '../utils/is-positive'
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 = 'unexpectedVariableDeclarationsOrder'

type Options = [
Partial<{
type: 'alphabetical' | 'line-length' | 'natural'
order: 'desc' | 'asc'
ignoreCase: boolean
}>,
]

export default createEslintRule<Options, MESSAGE_ID>({
name: 'sort-variable-declarations',
meta: {
type: 'suggestion',
docs: {
description: 'Enforce sorted variable declarations.',
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
type: {
description: 'Specifies the sorting method.',
type: 'string',
enum: ['alphabetical', 'natural', 'line-length'],
},
order: {
description:
'Determines whether the sorted items should be in ascending or descending order.',
type: 'string',
enum: ['asc', 'desc'],
},
ignoreCase: {
description:
'Controls whether sorting should be case-sensitive or not.',
type: 'boolean',
},
},
additionalProperties: false,
},
],
messages: {
unexpectedVariableDeclarationsOrder:
'Expected "{{right}}" to come before "{{left}}".',
},
},
defaultOptions: [
{
type: 'alphabetical',
order: 'asc',
ignoreCase: true,
},
],
create: context => ({
VariableDeclaration: node => {
if (node.declarations.length > 1) {
let options = complete(context.options.at(0), {
type: 'alphabetical',
ignoreCase: true,
order: 'asc',
} as const)

let sourceCode = getSourceCode(context)

let extractDependencies = (
init: TSESTree.Expression | null,
): string[] => {
if (!init) {
return []
}
let dependencies: string[] = []

let checkNode = (nodeValue: TSESTree.Node) => {
if (nodeValue.type === 'Identifier') {
dependencies.push(nodeValue.name)
}

if (
'body' in nodeValue &&
nodeValue.body &&
!Array.isArray(nodeValue.body)
) {
traverseNode(nodeValue.body)
}

if ('left' in nodeValue) {
traverseNode(nodeValue.left)
}

if ('right' in nodeValue) {
traverseNode(nodeValue.right as TSESTree.Node)
}

if ('elements' in nodeValue) {
nodeValue.elements
.filter(currentNode => currentNode !== null)
.forEach(traverseNode)
} else if ('arguments' in nodeValue) {
nodeValue.arguments.forEach(traverseNode)
}
}

let traverseNode = (nodeValue: TSESTree.Node) => {
checkNode(nodeValue)
}

traverseNode(init)
return dependencies
}

let nodes = node.declarations.map((declaration): SortingNode => {
let name

if (
declaration.id.type === 'ArrayPattern' ||
declaration.id.type === 'ObjectPattern'
) {
name = sourceCode.text.slice(...declaration.id.range)
} else {
;({ name } = declaration.id)
}

let dependencies = extractDependencies(declaration.init)

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

pairwise(nodes, (left, right) => {
if (isPositive(compare(left, right, options))) {
context.report({
messageId: 'unexpectedVariableDeclarationsOrder',
data: {
left: toSingleLine(left.name),
right: toSingleLine(right.name),
},
node: right.node,
fix: fixer =>
makeFixes(fixer, nodes, sortNodes(nodes, options), sourceCode),
})
}
})
}
},
}),
})
Loading

0 comments on commit 12bd265

Please sign in to comment.