diff --git a/docs/content/rules/sort-heritage-clauses.mdx b/docs/content/rules/sort-heritage-clauses.mdx
new file mode 100644
index 000000000..a916260e3
--- /dev/null
+++ b/docs/content/rules/sort-heritage-clauses.mdx
@@ -0,0 +1,249 @@
+---
+title: sort-heritage-clauses
+description: Enforce sorting of heritage clauses for improved readability and maintainability. Use this ESLint rule to keep your heritage clauses well-organized
+shortDescription: Enforce sorted heritage clauses
+keywords:
+ - eslint
+ - sort heritage clauses
+ - sort implements
+ - sort extends
+ - eslint rule
+ - coding standards
+ - code quality
+ - javascript linting
+ - typescript heritage clauses sorting
+---
+
+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 heritage clauses.
+
+This rule detects instances where heritage clauses are not sorted and raises linting errors, encouraging developers to arrange elements in the desired order.
+
+## Try it out
+
+
+
+## Options
+
+This rule accepts an options object with the following properties:
+
+### type
+
+default: `'alphabetical'`
+
+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
+
+default: `'asc'`
+
+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
+
+default: `true`
+
+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”).
+
+### specialCharacters
+
+default: `keep`
+
+Controls whether special characters should be trimmed, removed or kept before sorting.
+
+- `'keep'` — Keep special characters when sorting (e.g., “_a” comes before “a”).
+- `'trim'` — Trim special characters when sorting alphabetically or naturally (e.g., “_a” and “a” are the same).
+- `'remove'` — Remove special characters when sorting (e.g., “/a/b” and “ab” are the same).
+
+### groups
+
+
+ type: `Array`
+
+default: `[]`
+
+Allows you to specify a list of heritage clause groups for sorting.
+
+Predefined groups:
+
+- `'unknown'` — Heritage Clauses that don’t fit into any group specified in the `groups` option.
+
+If the `unknown` group is not specified in the `groups` option, it will automatically be added to the end of the list.
+
+Each heritage clause will be assigned a single group specified in the `groups` option (or the `unknown` group if no match is found).
+The order of items in the `groups` option determines how groups are ordered.
+
+Within a given group, members will be sorted according to the `type`, `order`, `ignoreCase`, etc. options.
+
+Individual groups can be combined together by placing them in an array. The order of groups in that array does not matter.
+All members of the groups in the array will be sorted together as if they were part of a single group.
+
+### customGroups
+
+
+ type: `{ [groupName: string]: string | string[] }`
+
+default: `{}`
+
+You can define your own groups and use custom glob patterns or regex to match specific heritage clauses.
+
+Use the `matcher` option to specify the pattern matching method.
+
+Each key of `customGroups` represents a group name which you can then use in the `groups` option. The value for each key can either be of type:
+- `string` — A heritage clause's name matching the value will be marked as part of the group referenced by the key.
+- `string[]` — A heritage clause's name matching any of the values of the array will be marked as part of the group referenced by the key.
+The order of values in the array does not matter.
+
+Custom group matching takes precedence over predefined group matching.
+
+#### Example:
+
+Put the `WithId` clause before anything else:
+
+```ts
+interface Interface extends WithId, Logged, StartupService {
+ // ...
+}
+```
+
+`groups` and `customGroups` configuration:
+
+```js
+ {
+ groups: [
+ 'withIdInterface', // [!code ++]
+ 'unknown'
+ ],
++ customGroups: { // [!code ++]
++ withIdInterface: 'WithId' // [!code ++]
++ } // [!code ++]
+ }
+```
+
+### matcher
+
+default: `'minimatch'`
+
+Determines the matcher used for patterns in the `customGroups` option.
+
+- `'minimatch'` — Use the [minimatch](https://github.com/isaacs/minimatch) library for pattern matching.
+- `'regex'` — Use regular expressions for pattern matching.
+
+## Usage
+
+
+
+## Version
+
+This rule was introduced in [v4.0.0](https://github.com/azat-io/eslint-plugin-perfectionist/releases/tag/v4.0.0).
+
+## Resources
+
+- [Rule source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/rules/sort-heritage-clauses.ts)
+- [Test source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/test/sort-heritage-clauses.test.ts)
diff --git a/index.ts b/index.ts
index bedccff65..a98b3c814 100644
--- a/index.ts
+++ b/index.ts
@@ -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'
@@ -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,
diff --git a/readme.md b/readme.md
index 4e4f5d0d4..d408e23e8 100644
--- a/readme.md
+++ b/readme.md
@@ -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 | 🔧 |
diff --git a/rules/sort-heritage-clauses.ts b/rules/sort-heritage-clauses.ts
new file mode 100644
index 000000000..4549b6978
--- /dev/null
+++ b/rules/sort-heritage-clauses.ts
@@ -0,0 +1,227 @@
+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 = 'unknown' | T[number]
+
+export type Options = [
+ Partial<{
+ customGroups: { [key in T[number]]: string[] | string }
+ type: 'alphabetical' | 'line-length' | 'natural'
+ specialCharacters: 'remove' | 'trim' | 'keep'
+ groups: (Group[] | Group)[]
+ matcher: 'minimatch' | 'regex'
+ order: 'desc' | 'asc'
+ ignoreCase: boolean
+ }>,
+]
+
+export default createEslintRule, 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>>,
+ options: Required[0]>,
+ heritageClauses:
+ | TSESTree.TSInterfaceHeritage[]
+ | TSESTree.TSClassImplements[],
+) => {
+ if (heritageClauses.length < 2) {
+ return
+ }
+ let sourceCode = getSourceCode(context)
+
+ let formattedNodes: SortingNode[] = heritageClauses.map(heritageClause => {
+ let name = getHeritageClauseExpressionName(heritageClause.expression)
+
+ let { getGroup, setCustomGroups } = useGroups(options)
+ setCustomGroups(options.customGroups, name)
+
+ return {
+ size: rangeToDiff(heritageClause.range),
+ node: heritageClause,
+ group: getGroup(),
+ name,
+ }
+ })
+
+ let sortedNodes = sortNodesByGroups(formattedNodes, options)
+ pairwise(formattedNodes, (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, formattedNodes, sortedNodes, sourceCode),
+ })
+ })
+}
+
+const getHeritageClauseExpressionName = (
+ expression: TSESTree.PrivateIdentifier | TSESTree.Expression,
+) => {
+ if (expression.type === 'Identifier') {
+ return expression.name
+ }
+ if ('property' in expression) {
+ return getHeritageClauseExpressionName(expression.property)
+ /* c8 ignore start - should never throw */
+ }
+ throw new Error(
+ 'Unexpected heritage clause expression. Please report this issue ' +
+ 'here: https://github.com/azat-io/eslint-plugin-perfectionist/issues',
+ )
+ /* c8 ignore end */
+}
diff --git a/test/sort-heritage-clauses.test.ts b/test/sort-heritage-clauses.test.ts
new file mode 100644
index 000000000..2a71dfc02
--- /dev/null
+++ b/test/sort-heritage-clauses.test.ts
@@ -0,0 +1,991 @@
+import { RuleTester } from '@typescript-eslint/rule-tester'
+import { afterAll, describe, it } from 'vitest'
+import { dedent } from 'ts-dedent'
+
+import rule from '../rules/sort-heritage-clauses'
+
+let ruleName = 'sort-heritage-clauses'
+
+describe(ruleName, () => {
+ RuleTester.describeSkip = describe.skip
+ RuleTester.afterAll = afterAll
+ RuleTester.describe = describe
+ RuleTester.itOnly = it.only
+ RuleTester.itSkip = it.skip
+ RuleTester.it = it
+
+ let ruleTester = new RuleTester()
+
+ describe(`${ruleName}: sorting by alphabetical order`, () => {
+ let type = 'alphabetical-order'
+
+ let options = {
+ type: 'alphabetical',
+ ignoreCase: true,
+ order: 'asc',
+ } as const
+
+ ruleTester.run(`${ruleName}(${type}): sorts heritage clauses`, rule, {
+ valid: [
+ {
+ code: dedent`
+ interface Interface extends
+ a,
+ b,
+ c {
+ }
+ `,
+ options: [options],
+ },
+ {
+ code: dedent`
+ interface Interface extends
+ a {
+ }
+ `,
+ options: [options],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ a,
+ c,
+ b {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ a,
+ b,
+ c {
+ }
+ `,
+ options: [options],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'c',
+ right: 'b',
+ },
+ },
+ ],
+ },
+ {
+ code: dedent`
+ interface Interface extends
+ A.a,
+ C.c,
+ B.b {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ A.a,
+ B.b,
+ C.c {
+ }
+ `,
+ options: [options],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'c',
+ right: 'b',
+ },
+ },
+ ],
+ },
+ ],
+ })
+
+ ruleTester.run(`${ruleName}(${type}): does not break docs`, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ /**
+ * Comment B
+ */
+ b,
+ /**
+ * Comment A
+ */
+ a,
+ // Comment D
+ d,
+ /* Comment C */
+ c {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ /**
+ * Comment A
+ */
+ a,
+ /**
+ * Comment B
+ */
+ b,
+ /* Comment C */
+ c,
+ // Comment D
+ d {
+ }
+ `,
+ options: [options],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'b',
+ right: 'a',
+ },
+ },
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'd',
+ right: 'c',
+ },
+ },
+ ],
+ },
+ ],
+ })
+
+ ruleTester.run(
+ `${ruleName}(${type}): sorts heritage clauses with comments on the same line`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ b // Comment B
+ , a // Comment A
+ {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ a // Comment A
+ , b // Comment B
+ {
+ }
+ `,
+ options: [options],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'b',
+ right: 'a',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to set groups for sorting`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface Interface extends
+ g,
+ a {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ groups: ['g'],
+ customGroups: {
+ g: 'g',
+ },
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ a,
+ g {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ g,
+ a {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ groups: ['g'],
+ customGroups: {
+ g: 'g',
+ },
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesGroupOrder',
+ data: {
+ left: 'a',
+ leftGroup: 'unknown',
+ right: 'g',
+ rightGroup: 'g',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to use regex matcher for custom groups`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface Interface extends
+ iHaveFooInMyName,
+ meTooIHaveFoo,
+ a,
+ b {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ matcher: 'regex',
+ groups: ['unknown', 'elementsWithoutFoo'],
+ customGroups: {
+ elementsWithoutFoo: '^(?!.*Foo).*$',
+ },
+ },
+ ],
+ },
+ ],
+ invalid: [],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to trim special characters`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface MyInterface extends
+ _a,
+ b,
+ _c {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ specialCharacters: 'trim',
+ },
+ ],
+ },
+ ],
+ invalid: [],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to remove special characters`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface MyInterface extends
+ ab,
+ a_c {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ specialCharacters: 'remove',
+ },
+ ],
+ },
+ ],
+ invalid: [],
+ },
+ )
+ })
+
+ describe(`${ruleName}: sorting by natural order`, () => {
+ let type = 'natural-order'
+
+ let options = {
+ type: 'natural',
+ ignoreCase: true,
+ order: 'asc',
+ } as const
+
+ ruleTester.run(`${ruleName}(${type}): sorts heritage clauses`, rule, {
+ valid: [
+ {
+ code: dedent`
+ interface Interface extends
+ a,
+ b,
+ c {
+ }
+ `,
+ options: [options],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ a,
+ c,
+ b {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ a,
+ b,
+ c {
+ }
+ `,
+ options: [options],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'c',
+ right: 'b',
+ },
+ },
+ ],
+ },
+ ],
+ })
+
+ ruleTester.run(`${ruleName}(${type}): does not break docs`, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ /**
+ * Comment B
+ */
+ b,
+ /**
+ * Comment A
+ */
+ a,
+ // Comment D
+ d,
+ /* Comment C */
+ c {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ /**
+ * Comment A
+ */
+ a,
+ /**
+ * Comment B
+ */
+ b,
+ /* Comment C */
+ c,
+ // Comment D
+ d {
+ }
+ `,
+ options: [options],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'b',
+ right: 'a',
+ },
+ },
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'd',
+ right: 'c',
+ },
+ },
+ ],
+ },
+ ],
+ })
+
+ ruleTester.run(
+ `${ruleName}(${type}): sorts heritage clauses with comments on the same line`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ b // Comment B
+ , a // Comment A
+ {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ a // Comment A
+ , b // Comment B
+ {
+ }
+ `,
+ options: [options],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'b',
+ right: 'a',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to set groups for sorting`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface Interface extends
+ g,
+ a {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ groups: ['g'],
+ customGroups: {
+ g: 'g',
+ },
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ a,
+ g {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ g,
+ a {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ groups: ['g'],
+ customGroups: {
+ g: 'g',
+ },
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesGroupOrder',
+ data: {
+ left: 'a',
+ leftGroup: 'unknown',
+ right: 'g',
+ rightGroup: 'g',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to use regex matcher for custom groups`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface Interface extends
+ iHaveFooInMyName,
+ meTooIHaveFoo,
+ a,
+ b {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ matcher: 'regex',
+ groups: ['unknown', 'elementsWithoutFoo'],
+ customGroups: {
+ elementsWithoutFoo: '^(?!.*Foo).*$',
+ },
+ },
+ ],
+ },
+ ],
+ invalid: [],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to trim special characters`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface MyInterface extends
+ _a,
+ b,
+ _c {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ specialCharacters: 'trim',
+ },
+ ],
+ },
+ ],
+ invalid: [],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to remove special characters`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface MyInterface extends
+ ab,
+ a_c {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ specialCharacters: 'remove',
+ },
+ ],
+ },
+ ],
+ invalid: [],
+ },
+ )
+ })
+
+ describe(`${ruleName}: sorting by line length`, () => {
+ let type = 'line-length-order'
+
+ let options = {
+ type: 'line-length',
+ ignoreCase: true,
+ order: 'desc',
+ } as const
+
+ ruleTester.run(`${ruleName}(${type}): sorts heritage clauses`, rule, {
+ valid: [
+ {
+ code: dedent`
+ interface Interface extends
+ aaa,
+ bb,
+ c {
+ }
+ `,
+ options: [options],
+ },
+ {
+ code: dedent`
+ class Class implements
+ aaa,
+ bb,
+ c {
+ }
+ `,
+ options: [options],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ aaa,
+ c,
+ bb {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ aaa,
+ bb,
+ c {
+ }
+ `,
+ options: [options],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'c',
+ right: 'bb',
+ },
+ },
+ ],
+ },
+ {
+ code: dedent`
+ class Class implements
+ aaa,
+ c,
+ bb {
+ }
+ `,
+ output: dedent`
+ class Class implements
+ aaa,
+ bb,
+ c {
+ }
+ `,
+ options: [options],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'c',
+ right: 'bb',
+ },
+ },
+ ],
+ },
+ ],
+ })
+
+ ruleTester.run(`${ruleName}(${type}): does not break docs`, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ /**
+ * Comment B
+ */
+ bbb,
+ /**
+ * Comment A
+ */
+ aaaa,
+ // Comment D
+ d,
+ /* Comment C */
+ cc {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ /**
+ * Comment A
+ */
+ aaaa,
+ /**
+ * Comment B
+ */
+ bbb,
+ /* Comment C */
+ cc,
+ // Comment D
+ d {
+ }
+ `,
+ options: [options],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'bbb',
+ right: 'aaaa',
+ },
+ },
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'd',
+ right: 'cc',
+ },
+ },
+ ],
+ },
+ ],
+ })
+
+ ruleTester.run(
+ `${ruleName}(${type}): sorts heritage clauses with comments on the same line`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ b // Comment B
+ , aa // Comment A
+ {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ aa // Comment A
+ , b // Comment B
+ {
+ }
+ `,
+ options: [options],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'b',
+ right: 'aa',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to set groups for sorting`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface Interface extends
+ g,
+ aa {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ groups: ['g'],
+ customGroups: {
+ g: 'g',
+ },
+ },
+ ],
+ },
+ ],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ aa,
+ g {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ g,
+ aa {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ groups: ['g'],
+ customGroups: {
+ g: 'g',
+ },
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesGroupOrder',
+ data: {
+ left: 'aa',
+ leftGroup: 'unknown',
+ right: 'g',
+ rightGroup: 'g',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to use regex matcher for custom groups`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface Interface extends
+ iHaveFooInMyName,
+ meTooIHaveFoo,
+ a,
+ b {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ matcher: 'regex',
+ groups: ['unknown', 'elementsWithoutFoo'],
+ customGroups: {
+ elementsWithoutFoo: '^(?!.*Foo).*$',
+ },
+ },
+ ],
+ },
+ ],
+ invalid: [],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to trim special characters`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface MyInterface extends
+ _aaa,
+ bb,
+ _c {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ specialCharacters: 'trim',
+ },
+ ],
+ },
+ ],
+ invalid: [],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}(${type}): allows to remove special characters`,
+ rule,
+ {
+ valid: [
+ {
+ code: dedent`
+ interface MyInterface extends
+ aaa,
+ a_c {
+ }
+ `,
+ options: [
+ {
+ ...options,
+ specialCharacters: 'remove',
+ },
+ ],
+ },
+ ],
+ invalid: [],
+ },
+ )
+ })
+
+ describe(`${ruleName}: misc`, () => {
+ ruleTester.run(
+ `${ruleName}: sets alphabetical asc sorting as default`,
+ rule,
+ {
+ valid: [
+ dedent`
+ interface Interface extends
+ a,
+ b {
+ }
+ `,
+ ],
+ invalid: [
+ {
+ code: dedent`
+ interface Interface extends
+ b,
+ a {
+ }
+ `,
+ output: dedent`
+ interface Interface extends
+ a,
+ b {
+ }
+ `,
+ errors: [
+ {
+ messageId: 'unexpectedHeritageClausesOrder',
+ data: {
+ left: 'b',
+ right: 'a',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+ })
+})