Skip to content

Commit

Permalink
feat: adds sort-heritage-clauses
Browse files Browse the repository at this point in the history
  • Loading branch information
hugop95 committed Oct 21, 2024
1 parent 3ceb793 commit 5110dfa
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 20 deletions.
2 changes: 2 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {

import sortVariableDeclarations from './rules/sort-variable-declarations'
import sortIntersectionTypes from './rules/sort-intersection-types'
import sortHeritageClauses from './rules/sort-heritage-clauses'
import sortArrayIncludes from './rules/sort-array-includes'
import sortNamedImports from './rules/sort-named-imports'
import sortNamedExports from './rules/sort-named-exports'
Expand Down Expand Up @@ -37,6 +38,7 @@ let plugin = {
rules: {
'sort-variable-declarations': sortVariableDeclarations,
'sort-intersection-types': sortIntersectionTypes,
'sort-heritage-clauses': sortHeritageClauses,
'sort-array-includes': sortArrayIncludes,
'sort-named-imports': sortNamedImports,
'sort-named-exports': sortNamedExports,
Expand Down
41 changes: 21 additions & 20 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,26 +172,27 @@ module.exports = {

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

| Name | Description | 🔧 |
| :--------------------------------------------------------------------------------------- | :------------------------------------------ | :-- |
| [sort-array-includes](https://perfectionist.dev/rules/sort-array-includes) | Enforce sorted arrays before include method | 🔧 |
| [sort-classes](https://perfectionist.dev/rules/sort-classes) | Enforce sorted classes | 🔧 |
| [sort-decorators](https://perfectionist.dev/rules/sort-decorators) | Enforce sorted decorators | 🔧 |
| [sort-enums](https://perfectionist.dev/rules/sort-enums) | Enforce sorted TypeScript enums | 🔧 |
| [sort-exports](https://perfectionist.dev/rules/sort-exports) | Enforce sorted exports | 🔧 |
| [sort-imports](https://perfectionist.dev/rules/sort-imports) | Enforce sorted imports | 🔧 |
| [sort-interfaces](https://perfectionist.dev/rules/sort-interfaces) | Enforce sorted interface properties | 🔧 |
| [sort-intersection-types](https://perfectionist.dev/rules/sort-intersection-types) | Enforce sorted intersection types | 🔧 |
| [sort-jsx-props](https://perfectionist.dev/rules/sort-jsx-props) | Enforce sorted JSX props | 🔧 |
| [sort-maps](https://perfectionist.dev/rules/sort-maps) | Enforce sorted Map elements | 🔧 |
| [sort-named-exports](https://perfectionist.dev/rules/sort-named-exports) | Enforce sorted named exports | 🔧 |
| [sort-named-imports](https://perfectionist.dev/rules/sort-named-imports) | Enforce sorted named imports | 🔧 |
| [sort-object-types](https://perfectionist.dev/rules/sort-object-types) | Enforce sorted object types | 🔧 |
| [sort-objects](https://perfectionist.dev/rules/sort-objects) | Enforce sorted objects | 🔧 |
| [sort-sets](https://perfectionist.dev/rules/sort-sets) | Enforce sorted Set elements | 🔧 |
| [sort-switch-case](https://perfectionist.dev/rules/sort-switch-case) | Enforce sorted switch case statements | 🔧 |
| [sort-union-types](https://perfectionist.dev/rules/sort-union-types) | Enforce sorted union types | 🔧 |
| [sort-variable-declarations](https://perfectionist.dev/rules/sort-variable-declarations) | Enforce sorted variable declarations | 🔧 |
| Name | Description | 🔧 |
| :--------------------------------------------------------------------------------------- | :-------------------------------------------- | :-- |
| [sort-array-includes](https://perfectionist.dev/rules/sort-array-includes) | Enforce sorted arrays before include method | 🔧 |
| [sort-classes](https://perfectionist.dev/rules/sort-classes) | Enforce sorted classes | 🔧 |
| [sort-decorators](https://perfectionist.dev/rules/sort-decorators) | Enforce sorted decorators | 🔧 |
| [sort-enums](https://perfectionist.dev/rules/sort-enums) | Enforce sorted TypeScript enums | 🔧 |
| [sort-exports](https://perfectionist.dev/rules/sort-exports) | Enforce sorted exports | 🔧 |
| [sort-heritage-clauses](https://perfectionist.dev/rules/sort-heritage-clauses) | Enforce sorted `implements`/`extends` clauses | 🔧 |
| [sort-imports](https://perfectionist.dev/rules/sort-imports) | Enforce sorted imports | 🔧 |
| [sort-interfaces](https://perfectionist.dev/rules/sort-interfaces) | Enforce sorted interface properties | 🔧 |
| [sort-intersection-types](https://perfectionist.dev/rules/sort-intersection-types) | Enforce sorted intersection types | 🔧 |
| [sort-jsx-props](https://perfectionist.dev/rules/sort-jsx-props) | Enforce sorted JSX props | 🔧 |
| [sort-maps](https://perfectionist.dev/rules/sort-maps) | Enforce sorted Map elements | 🔧 |
| [sort-named-exports](https://perfectionist.dev/rules/sort-named-exports) | Enforce sorted named exports | 🔧 |
| [sort-named-imports](https://perfectionist.dev/rules/sort-named-imports) | Enforce sorted named imports | 🔧 |
| [sort-object-types](https://perfectionist.dev/rules/sort-object-types) | Enforce sorted object types | 🔧 |
| [sort-objects](https://perfectionist.dev/rules/sort-objects) | Enforce sorted objects | 🔧 |
| [sort-sets](https://perfectionist.dev/rules/sort-sets) | Enforce sorted Set elements | 🔧 |
| [sort-switch-case](https://perfectionist.dev/rules/sort-switch-case) | Enforce sorted switch case statements | 🔧 |
| [sort-union-types](https://perfectionist.dev/rules/sort-union-types) | Enforce sorted union types | 🔧 |
| [sort-variable-declarations](https://perfectionist.dev/rules/sort-variable-declarations) | Enforce sorted variable declarations | 🔧 |

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

Expand Down
216 changes: 216 additions & 0 deletions rules/sort-heritage-clauses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'
import type { TSESTree } from '@typescript-eslint/types'

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

import { validateGroupsConfiguration } from '../utils/validate-groups-configuration'
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
import { createEslintRule } from '../utils/create-eslint-rule'
import { getGroupNumber } from '../utils/get-group-number'
import { getSourceCode } from '../utils/get-source-code'
import { toSingleLine } from '../utils/to-single-line'
import { rangeToDiff } from '../utils/range-to-diff'
import { getSettings } from '../utils/get-settings'
import { useGroups } from '../utils/use-groups'
import { makeFixes } from '../utils/make-fixes'
import { complete } from '../utils/complete'
import { pairwise } from '../utils/pairwise'

type MESSAGE_ID =
| 'unexpectedHeritageClausesGroupOrder'
| 'unexpectedHeritageClausesOrder'

type Group<T extends string[]> = 'unknown' | T[number]

export type Options<T extends string[]> = [
Partial<{
customGroups: { [key in T[number]]: string[] | string }
type: 'alphabetical' | 'line-length' | 'natural'
specialCharacters: 'remove' | 'trim' | 'keep'
groups: (Group<T>[] | Group<T>)[]
matcher: 'minimatch' | 'regex'
order: 'desc' | 'asc'
ignoreCase: boolean
}>,
]

export default createEslintRule<Options<string[]>, MESSAGE_ID>({
name: 'sort-heritage-clauses',
meta: {
type: 'suggestion',
docs: {
description: 'Enforce sorted heritage clauses.',
},
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'],
},
matcher: {
description: 'Specifies the string matcher.',
type: 'string',
enum: ['minimatch', 'regex'],
},
ignoreCase: {
description:
'Controls whether sorting should be case-sensitive or not.',
type: 'boolean',
},
specialCharacters: {
description:
'Controls how special characters should be handled before sorting.',
type: 'string',
enum: ['remove', 'trim', 'keep'],
},
groups: {
description: 'Specifies the order of the groups.',
type: 'array',
items: {
oneOf: [
{
type: 'string',
},
{
type: 'array',
items: {
type: 'string',
},
},
],
},
},
customGroups: {
description: 'Specifies custom groups.',
type: 'object',
additionalProperties: {
oneOf: [
{
type: 'string',
},
{
type: 'array',
items: {
type: 'string',
},
},
],
},
},
},
additionalProperties: false,
},
],
messages: {
unexpectedHeritageClausesGroupOrder:
'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
unexpectedHeritageClausesOrder:
'Expected "{{right}}" to come before "{{left}}".',
},
},
defaultOptions: [
{
type: 'alphabetical',
order: 'asc',
ignoreCase: true,
specialCharacters: 'keep',
matcher: 'minimatch',
groups: [],
customGroups: {},
},
],
create: context => {
let settings = getSettings(context.settings)

let options = complete(context.options.at(0), settings, {
type: 'alphabetical',
matcher: 'minimatch',
ignoreCase: true,
specialCharacters: 'keep',
customGroups: {},
order: 'asc',
groups: [],
} as const)

validateGroupsConfiguration(
options.groups,
['unknown'],
Object.keys(options.customGroups),
)

return {
ClassDeclaration: declaration =>
sortHeritageClauses(context, options, declaration.implements),
TSInterfaceDeclaration: declaration =>
sortHeritageClauses(context, options, declaration.extends),
}
},
})

const sortHeritageClauses = (
context: Readonly<RuleContext<MESSAGE_ID, Options<string[]>>>,
options: Required<Options<string[]>[0]>,
heritageClauses:
| TSESTree.TSInterfaceHeritage[]
| TSESTree.TSClassImplements[],
) => {
if (heritageClauses.length < 2) {
return
}
let sourceCode = getSourceCode(context)

let formattedMembers: SortingNode[] = heritageClauses
.map(heritageClause => {
if (heritageClause.expression.type !== 'Identifier') {
return null
}
let { name } = heritageClause.expression

let { getGroup, setCustomGroups } = useGroups(options)
setCustomGroups(options.customGroups, name)

return {
size: rangeToDiff(heritageClause.range),
node: heritageClause,
group: getGroup(),
name,
}
})
.filter(sortingNode => !!sortingNode)

let sortedNodes = sortNodesByGroups(formattedMembers, options)
let nodes = formattedMembers.flat()
pairwise(nodes, (left, right) => {
let indexOfLeft = sortedNodes.indexOf(left)
let indexOfRight = sortedNodes.indexOf(right)
if (indexOfLeft <= indexOfRight) {
return
}
let leftNum = getGroupNumber(options.groups, left)
let rightNum = getGroupNumber(options.groups, right)
context.report({
messageId:
leftNum !== rightNum
? 'unexpectedHeritageClausesGroupOrder'
: 'unexpectedHeritageClausesOrder',
data: {
left: toSingleLine(left.name),
leftGroup: left.group,
right: toSingleLine(right.name),
rightGroup: right.group,
},
node: right.node,
fix: fixer => makeFixes(fixer, nodes, sortedNodes, sourceCode),
})
})
}

0 comments on commit 5110dfa

Please sign in to comment.