From 3ad40ff67369c5b6fb95a980c55d12c8eb8cac35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Tue, 9 Apr 2024 06:27:28 -0400 Subject: [PATCH] feat: add sort-intersection-types rule --- docs/rules/sort-intersection-types.md | 148 +++++ index.ts | 3 + readme.md | 37 +- rules/sort-intersection-types.ts | 110 ++++ test/sort-intersection-types.test.ts | 781 ++++++++++++++++++++++++++ 5 files changed, 1061 insertions(+), 18 deletions(-) create mode 100644 docs/rules/sort-intersection-types.md create mode 100644 rules/sort-intersection-types.ts create mode 100644 test/sort-intersection-types.test.ts diff --git a/docs/rules/sort-intersection-types.md b/docs/rules/sort-intersection-types.md new file mode 100644 index 000000000..2cc87f4e5 --- /dev/null +++ b/docs/rules/sort-intersection-types.md @@ -0,0 +1,148 @@ +--- +title: sort-intersection-types +description: ESLint Plugin Perfectionist rule which enforce sorted intersection types in TypeScript +--- + +# sort-intersection-types + +💼 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). + + + +## 📖 Rule Details + +Enforce sorted intersection types in TypeScript. + +Adhering to the `sort-intersection-types` rule enables developers to ensure that intersection types are consistently sorted, resulting in cleaner and more maintainable code. This rule promotes a standardized ordering of intersection types, making it easier for developers to navigate and understand the structure of type intersections within the codebase. + +:::info Important +If you use the [`sort-type-constituents`](https://typescript-eslint.io/rules/sort-type-constituents) rule from the [`@typescript-eslint/eslint-plugin`](https://typescript-eslint.io) plugin, it is highly recommended to [disable it](https://eslint.org/docs/latest/use/configure/rules#using-configuration-files-1) to avoid conflicts. +::: + +## 💡 Examples + +::: code-group + + +```ts [Alphabetical and Natural Sorting] +// ❌ Incorrect +type NetworkState = + & Failed + & LoadedFromCache + & Success + & DataLoading + +// ✅ Correct +type NetworkState = + & DataLoading + & Failed + & LoadedFromCache + & Success +``` + + +```ts [Sorting by Line Length] +// ❌ Incorrect +type NetworkState = + & Failed + & LoadedFromCache + & Success + & DataLoading + +// ✅ Correct +type NetworkState = + & LoadedFromCache + & DataLoading + & Success + & Failed +``` + +::: + +## 🔧 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 + +(default: `'alphabetical'`) + +- `alphabetical` - sort alphabetically. +- `natural` - sort in natural order. +- `line-length` - sort by code line length. + +### order + +(default: `'asc'`) + +- `asc` - enforce properties to be in ascending order. +- `desc` - enforce properties to be in descending order. + +### ignore-case + +(default: `false`) + +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-intersection-types": [ + "error", + { + "type": "natural", + "order": "asc" + } + ] + } +} +``` + +```js [Flat Config] +// eslint.config.js +import perfectionist from 'eslint-plugin-perfectionist' + +export default [ + { + plugins: { + perfectionist, + }, + rules: { + 'perfectionist/sort-intersection-types': [ + 'error', + { + type: 'natural', + order: 'asc', + }, + ], + }, + }, +] +``` + +::: + +## 🚀 Version + +This rule was introduced in v0.4.0. + +## 📚 Resources + +- [Rule source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/rules/sort-intersection-types.ts) +- [Test source](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/test/sort-intersection-types.test.ts) diff --git a/index.ts b/index.ts index cc1cd3ba9..fc313e5fb 100644 --- a/index.ts +++ b/index.ts @@ -1,3 +1,4 @@ +import sortIntersectionTypes, { RULE_NAME as sortIntersectionTypesName } from './rules/sort-intersection-types' import sortSvelteAttributes, { RULE_NAME as sortSvelteAttributesName } from './rules/sort-svelte-attributes' import sortAstroAttributes, { RULE_NAME as sortAstroAttributesName } from './rules/sort-astro-attributes' import sortArrayIncludes, { RULE_NAME as sortArrayIncludesName } from './rules/sort-array-includes' @@ -84,6 +85,7 @@ let createConfigWithOptions = (options: { 'spread-last': true, }, ], + [sortIntersectionTypesName]: ['error'], [sortSvelteAttributesName]: ['error'], [sortAstroAttributesName]: ['error'], [sortVueAttributesName]: ['error'], @@ -125,6 +127,7 @@ export default { [sortObjectTypesName]: sortObjectTypes, [sortObjectsName]: sortObjects, [sortSvelteAttributesName]: sortSvelteAttributes, + [sortIntersectionTypesName]: sortIntersectionTypes, [sortUnionTypesName]: sortUnionTypes, [sortVueAttributesName]: sortVueAttributes, }, diff --git a/readme.md b/readme.md index 6aca73060..9224738cc 100644 --- a/readme.md +++ b/readme.md @@ -138,24 +138,25 @@ export default [ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). -| Name | Description | 🔧 | -| :------------------------------------------------------------------------------------------------- | :------------------------------------------ | :-- | -| [sort-array-includes](https://eslint-plugin-perfectionist.azat.io/rules/sort-array-includes) | enforce sorted arrays before include method | 🔧 | -| [sort-astro-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-astro-attributes) | enforce sorted Astro attributes | 🔧 | -| [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 | 🔧 | -| [sort-maps](https://eslint-plugin-perfectionist.azat.io/rules/sort-maps) | enforce sorted Map elements | 🔧 | -| [sort-named-exports](https://eslint-plugin-perfectionist.azat.io/rules/sort-named-exports) | enforce sorted named exports | 🔧 | -| [sort-named-imports](https://eslint-plugin-perfectionist.azat.io/rules/sort-named-imports) | enforce sorted named imports | 🔧 | -| [sort-object-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-object-types) | enforce sorted object types | 🔧 | -| [sort-objects](https://eslint-plugin-perfectionist.azat.io/rules/sort-objects) | enforce sorted objects | 🔧 | -| [sort-svelte-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-svelte-attributes) | enforce sorted Svelte attributes | 🔧 | -| [sort-union-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-union-types) | enforce sorted union types | 🔧 | -| [sort-vue-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-vue-attributes) | enforce sorted Vue attributes | 🔧 | +| Name | Description | 🔧 | +| :--------------------------------------------------------------------------------------------------- | :------------------------------------------ | :-- | +| [sort-array-includes](https://eslint-plugin-perfectionist.azat.io/rules/sort-array-includes) | enforce sorted arrays before include method | 🔧 | +| [sort-astro-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-astro-attributes) | enforce sorted Astro attributes | 🔧 | +| [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 | 🔧 | +| [sort-maps](https://eslint-plugin-perfectionist.azat.io/rules/sort-maps) | enforce sorted Map elements | 🔧 | +| [sort-named-exports](https://eslint-plugin-perfectionist.azat.io/rules/sort-named-exports) | enforce sorted named exports | 🔧 | +| [sort-named-imports](https://eslint-plugin-perfectionist.azat.io/rules/sort-named-imports) | enforce sorted named imports | 🔧 | +| [sort-object-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-object-types) | enforce sorted object types | 🔧 | +| [sort-objects](https://eslint-plugin-perfectionist.azat.io/rules/sort-objects) | enforce sorted objects | 🔧 | +| [sort-svelte-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-svelte-attributes) | enforce sorted Svelte attributes | 🔧 | +| [sort-intersection-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-intersection-types) | enforce sorted intersection types | 🔧 | +| [sort-union-types](https://eslint-plugin-perfectionist.azat.io/rules/sort-union-types) | enforce sorted union types | 🔧 | +| [sort-vue-attributes](https://eslint-plugin-perfectionist.azat.io/rules/sort-vue-attributes) | enforce sorted Vue attributes | 🔧 | diff --git a/rules/sort-intersection-types.ts b/rules/sort-intersection-types.ts new file mode 100644 index 000000000..21d17c844 --- /dev/null +++ b/rules/sort-intersection-types.ts @@ -0,0 +1,110 @@ +import type { SortingNode } from '../typings' + +import { createEslintRule } from '../utils/create-eslint-rule' +import { toSingleLine } from '../utils/to-single-line' +import { rangeToDiff } from '../utils/range-to-diff' +import { isPositive } from '../utils/is-positive' +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 = 'unexpectedIntersectionTypesOrder' + +type Options = [ + Partial<{ + 'ignore-case': boolean + order: SortOrder + type: SortType + }>, +] + +export const RULE_NAME = 'sort-intersection-types' + +export default createEslintRule({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: 'enforce sorted intersection types', + }, + fixable: 'code', + schema: [ + { + type: 'object', + properties: { + type: { + enum: [ + SortType.alphabetical, + SortType.natural, + SortType['line-length'], + ], + default: SortType.alphabetical, + type: 'string', + }, + order: { + enum: [SortOrder.asc, SortOrder.desc], + default: SortOrder.asc, + type: 'string', + }, + 'ignore-case': { + type: 'boolean', + default: false, + }, + }, + additionalProperties: false, + }, + ], + messages: { + unexpectedIntersectionTypesOrder: + 'Expected "{{right}}" to come before "{{left}}"', + }, + }, + defaultOptions: [ + { + type: SortType.alphabetical, + order: SortOrder.asc, + }, + ], + create: context => ({ + TSIntersectionType: node => { + let options = complete(context.options.at(0), { + type: SortType.alphabetical, + 'ignore-case': false, + order: SortOrder.asc, + }) + + let nodes: SortingNode[] = node.types.map(type => ({ + group: + type.type === 'TSNullKeyword' || type.type === 'TSUndefinedKeyword' + ? 'nullable' + : 'unknown', + name: context.sourceCode.text.slice(...type.range), + size: rangeToDiff(type.range), + node: type, + })) + + pairwise(nodes, (left, right) => { + let compareValue = isPositive(compare(left, right, options)) + + if (compareValue) { + context.report({ + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: toSingleLine(left.name), + right: toSingleLine(right.name), + }, + node: right.node, + fix: fixer => { + let sortedNodes = sortNodes(nodes, options) + + return makeFixes(fixer, nodes, sortedNodes, context.sourceCode) + }, + }) + } + }) + }, + }), +}) diff --git a/test/sort-intersection-types.test.ts b/test/sort-intersection-types.test.ts new file mode 100644 index 000000000..9bffe8192 --- /dev/null +++ b/test/sort-intersection-types.test.ts @@ -0,0 +1,781 @@ +import { RuleTester } from '@typescript-eslint/rule-tester' +import { afterAll, describe, it } from 'vitest' +import { dedent } from 'ts-dedent' + +import rule, { RULE_NAME } from '../rules/sort-intersection-types' +import { SortOrder, SortType } from '../typings' + +describe(RULE_NAME, () => { + 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({ + parser: '@typescript-eslint/parser', + }) + + describe(`${RULE_NAME}: sorting by alphabetical order`, () => { + let type = 'alphabetical-order' + + let options = { + type: SortType.alphabetical, + order: SortOrder.asc, + 'ignore-case': false, + } + + ruleTester.run(`${RULE_NAME}(${type}: sorts intersection types`, rule, { + valid: [ + { + code: dedent` + type Eternity = { label: Fushi } & { label: Gugu } & { label: Joaan } & { label: Parona } + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + type Eternity = { label: Fushi } & { label: Joaan } & { label: Parona } & { label: Gugu } + `, + output: dedent` + type Eternity = { label: Fushi } & { label: Gugu } & { label: Joaan } & { label: Parona } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: '{ label: Parona }', + right: '{ label: Gugu }', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: sorts keyword intersection types`, rule, { + valid: [], + invalid: [ + { + code: dedent` + type Value = + & { booleanValue: boolean } + & { numberValue: number } + & { stringValue: string } + & { anyValue: any } + & { unknownValue: unknown } + & { nullValue: null } + & { undefinedValue: undefined } + & { neverValue: never } + & { voidValue: void } + & { bigintValue: bigint } + `, + output: dedent` + type Value = + & { anyValue: any } + & { bigintValue: bigint } + & { booleanValue: boolean } + & { neverValue: never } + & { nullValue: null } + & { numberValue: number } + & { stringValue: string } + & { undefinedValue: undefined } + & { unknownValue: unknown } + & { voidValue: void } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: '{ stringValue: string }', + right: '{ anyValue: any }', + }, + }, + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: '{ unknownValue: unknown }', + right: '{ nullValue: null }', + }, + }, + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: '{ undefinedValue: undefined }', + right: '{ neverValue: never }', + }, + }, + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: '{ voidValue: void }', + right: '{ bigintValue: bigint }', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: works with generics`, rule, { + valid: [], + invalid: [ + { + code: 'Omit', + output: 'Omit', + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'PsychicAbilities', + right: 'Power', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: works with type references`, rule, { + valid: [], + invalid: [ + { + code: 'type DemonSlayer = Tanjiro & Zenitsu & Inosuke', + output: 'type DemonSlayer = Inosuke & Tanjiro & Zenitsu', + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'Zenitsu', + right: 'Inosuke', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: works with type references`, rule, { + valid: [], + invalid: [ + { + code: dedent` + type Character = + & { name: IntelligentTitan, status: 'titan' } + & { name: ErenYeager, species: 'human' } + `, + output: dedent` + type Character = + & { name: ErenYeager, species: 'human' } + & { name: IntelligentTitan, status: 'titan' } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: "{ name: IntelligentTitan, status: 'titan' }", + right: "{ name: ErenYeager, species: 'human' }", + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: sorts intersections with parentheses`, rule, { + valid: [], + invalid: [ + { + code: dedent` + type HeroAssociation = { + team: + & Saitama + & (( + superstrike: () => void, + ) => Hero & Saitama) + & Hero + } + `, + output: dedent` + type HeroAssociation = { + team: + & (( + superstrike: () => void, + ) => Hero & Saitama) + & Hero + & Saitama + } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'Saitama', + right: '( superstrike: () => void, ) => Hero & Saitama', + }, + }, + ], + }, + ], + }) + + ruleTester.run( + `${RULE_NAME}: sorts intersections with comment at the end`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + type Step = { value1: 1 } & { value2: 2 } & { value4: 4 } & { value3: 3 } & { value5: 5 } & { value100: 100 }; // Exam step. Example: 3 + `, + output: dedent` + type Step = { value1: 1 } & { value100: 100 } & { value2: 2 } & { value3: 3 } & { value4: 4 } & { value5: 5 }; // Exam step. Example: 3 + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: '{ value4: 4 }', + right: '{ value3: 3 }', + }, + }, + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: '{ value5: 5 }', + right: '{ value100: 100 }', + }, + }, + ], + }, + ], + }, + ) + }) + + describe(`${RULE_NAME}: sorting by natural order`, () => { + let type = 'natural-order' + + let options = { + type: SortType.alphabetical, + order: SortOrder.asc, + 'ignore-case': false, + } + + ruleTester.run(`${RULE_NAME}(${type}: sorts intersection types`, rule, { + valid: [ + { + code: dedent` + type Eternity = Fushi & Gugu & Joaan & Parona + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + type Eternity = Fushi & Joaan & Parona & Gugu + `, + output: dedent` + type Eternity = Fushi & Gugu & Joaan & Parona + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'Parona', + right: 'Gugu', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: sorts keyword intersection types`, rule, { + valid: [], + invalid: [ + { + code: dedent` + type Value = + & boolean + & number + & string + & any + & unknown + & null + & undefined + & never + & void + & bigint + `, + output: dedent` + type Value = + & any + & bigint + & boolean + & never + & null + & number + & string + & undefined + & unknown + & void + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'string', + right: 'any', + }, + }, + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'unknown', + right: 'null', + }, + }, + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'undefined', + right: 'never', + }, + }, + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'void', + right: 'bigint', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: works with generics`, rule, { + valid: [], + invalid: [ + { + code: 'Omit', + output: 'Omit', + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'PsychicAbilities', + right: 'Power', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: works with type references`, rule, { + valid: [], + invalid: [ + { + code: 'type DemonSlayer = Tanjiro & Zenitsu & Inosuke', + output: 'type DemonSlayer = Inosuke & Tanjiro & Zenitsu', + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'Zenitsu', + right: 'Inosuke', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: works with type references`, rule, { + valid: [], + invalid: [ + { + code: dedent` + type Character = + & { name: IntelligentTitan, status: 'titan' } + & { name: ErenYeager, species: 'human' } + `, + output: dedent` + type Character = + & { name: ErenYeager, species: 'human' } + & { name: IntelligentTitan, status: 'titan' } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: "{ name: IntelligentTitan, status: 'titan' }", + right: "{ name: ErenYeager, species: 'human' }", + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: sorts intersections with parentheses`, rule, { + valid: [], + invalid: [ + { + code: dedent` + type HeroAssociation = { + team: + & Saitama + & (( + superstrike: () => void, + ) => Hero & Saitama) + & Hero + } + `, + output: dedent` + type HeroAssociation = { + team: + & (( + superstrike: () => void, + ) => Hero & Saitama) + & Hero + & Saitama + } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'Saitama', + right: '( superstrike: () => void, ) => Hero & Saitama', + }, + }, + ], + }, + ], + }) + + ruleTester.run( + `${RULE_NAME}: sorts intersections with comment at the end`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + type Step = { value1: 1 } & { value2: 2 } & { value4: 4 } & { value3: 3 } & { value5: 5 } & { value100: 100 }; // Exam step. Example: 3 + `, + output: dedent` + type Step = { value1: 1 } & { value100: 100 } & { value2: 2 } & { value3: 3 } & { value4: 4 } & { value5: 5 }; // Exam step. Example: 3 + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: '{ value4: 4 }', + right: '{ value3: 3 }', + }, + }, + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: '{ value5: 5 }', + right: '{ value100: 100 }', + }, + }, + ], + }, + ], + }, + ) + }) + + describe(`${RULE_NAME}: sorting by line length`, () => { + let type = 'line-length-order' + + let options = { + type: SortType['line-length'], + order: SortOrder.desc, + } + + ruleTester.run(`${RULE_NAME}(${type}: sorts intersection types`, rule, { + valid: [ + { + code: dedent` + type Eternity = Parona & Joaan & Fushi & Gugu + `, + options: [options], + }, + ], + invalid: [ + { + code: dedent` + type Eternity = Fushi & Joaan & Parona & Gugu + `, + output: dedent` + type Eternity = Parona & Fushi & Joaan & Gugu + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'Joaan', + right: 'Parona', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: sorts keyword intersection types`, rule, { + valid: [], + invalid: [ + { + code: dedent` + type Value = + & boolean + & number + & string + & any + & unknown + & null + & undefined + & never + & void + & bigint + `, + output: dedent` + type Value = + & undefined + & boolean + & unknown + & number + & string + & bigint + & never + & null + & void + & any + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'any', + right: 'unknown', + }, + }, + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'null', + right: 'undefined', + }, + }, + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'void', + right: 'bigint', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: works with generics`, rule, { + valid: [], + invalid: [ + { + code: 'Omit', + output: 'Omit', + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'Power', + right: 'PsychicAbilities', + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: works with type references`, rule, { + valid: [ + { + code: 'type DemonSlayer = Tanjiro & Zenitsu & Inosuke', + options: [options], + }, + ], + invalid: [], + }) + + ruleTester.run(`${RULE_NAME}: works with type references`, rule, { + valid: [], + invalid: [ + { + code: dedent` + type Character = + & { name: ErenYeager, species: 'human' } + & { name: IntelligentTitan, status: 'titan' } + `, + output: dedent` + type Character = + & { name: IntelligentTitan, status: 'titan' } + & { name: ErenYeager, species: 'human' } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: "{ name: ErenYeager, species: 'human' }", + right: "{ name: IntelligentTitan, status: 'titan' }", + }, + }, + ], + }, + ], + }) + + ruleTester.run(`${RULE_NAME}: sorts intersections with parentheses`, rule, { + valid: [], + invalid: [ + { + code: dedent` + type HeroAssociation = { + team: + & Saitama + & (( + superstrike: () => void, + ) => Hero & Saitama) + & Hero + } + `, + output: dedent` + type HeroAssociation = { + team: + & (( + superstrike: () => void, + ) => Hero & Saitama) + & Saitama + & Hero + } + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'Saitama', + right: '( superstrike: () => void, ) => Hero & Saitama', + }, + }, + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'Hero', + right: 'Saitama', + }, + }, + ], + }, + ], + }) + + ruleTester.run( + `${RULE_NAME}: sorts intersections with comment at the end`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + type Step = { value1: 1 } & { value2: 2 } & { value4: 4 } & { value3: 3 } & { value5: 5 } & { value100: 100 }; // Exam step. Example: 3 + `, + output: dedent` + type Step = { value100: 100 } & { value1: 1 } & { value2: 2 } & { value4: 4 } & { value3: 3 } & { value5: 5 }; // Exam step. Example: 3 + `, + options: [options], + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: '{ value5: 5 }', + right: '{ value100: 100 }', + }, + }, + ], + }, + ], + }, + ) + }) + + describe(`${RULE_NAME}: misc`, () => { + ruleTester.run( + `${RULE_NAME}: sets alphabetical asc sorting as default`, + rule, + { + valid: [ + dedent` + type SupportedNumberBase = NumberBase.BASE_10 & NumberBase.BASE_16 & NumberBase.BASE_2 + `, + { + code: dedent` + type SupportedNumberBase = NumberBase.BASE_10 & NumberBase.BASE_16 & NumberBase.BASE_2 + `, + options: [{}], + }, + ], + invalid: [ + { + code: dedent` + type SupportedNumberBase = NumberBase.BASE_2 & NumberBase.BASE_10 & NumberBase.BASE_16 + `, + output: dedent` + type SupportedNumberBase = NumberBase.BASE_10 & NumberBase.BASE_16 & NumberBase.BASE_2 + `, + errors: [ + { + messageId: 'unexpectedIntersectionTypesOrder', + data: { + left: 'NumberBase.BASE_2', + right: 'NumberBase.BASE_10', + }, + }, + ], + }, + ], + }, + ) + }) +})