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', + }, + }, + ], + }, + ], + }, + ) + }) +})