From eb784619b5814161a0b1a5aae5719634260c493a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Renato=20B=C3=B6hler?= Date: Thu, 14 Mar 2024 09:54:10 -0300 Subject: [PATCH] feat: add group-kind option on sort-named-imports and sort-named-exports --- docs/rules/sort-named-exports.md | 11 + docs/rules/sort-named-imports.md | 11 + rules/sort-named-exports.ts | 44 +++- rules/sort-named-imports.ts | 52 ++++- test/sort-named-exports.test.ts | 361 ++++++++++++++++++++++++++++++- test/sort-named-imports.test.ts | 361 ++++++++++++++++++++++++++++++- typings/index.ts | 6 + 7 files changed, 826 insertions(+), 20 deletions(-) diff --git a/docs/rules/sort-named-exports.md b/docs/rules/sort-named-exports.md index 34c551ff1..9bfc58bf8 100644 --- a/docs/rules/sort-named-exports.md +++ b/docs/rules/sort-named-exports.md @@ -69,6 +69,7 @@ This rule accepts an options object with the following properties: interface Options { type?: 'alphabetical' | 'natural' | 'line-length' order?: 'asc' | 'desc' + 'group-kind'?: 'mixed' | 'values-first' | 'types-first' 'ignore-case'?: boolean } ``` @@ -88,6 +89,16 @@ interface Options { - `asc` - enforce properties to be in ascending order. - `desc` - enforce properties to be in descending order. +### group-kind + +(default: `'mixed'`) + +Allows to group named exports by their kind, with value exports coming either before or after type exports. + +- `mixed` - does not group named exports by their kind +- `values-first` - groups all value exports before type exports +- `types-first` - groups all type exports before value exports + ### ignore-case (default: `false`) diff --git a/docs/rules/sort-named-imports.md b/docs/rules/sort-named-imports.md index a39487dfc..650cfd4d6 100644 --- a/docs/rules/sort-named-imports.md +++ b/docs/rules/sort-named-imports.md @@ -85,6 +85,7 @@ This rule accepts an options object with the following properties: interface Options { type?: 'alphabetical' | 'natural' | 'line-length' order?: 'asc' | 'desc' + 'group-kind'?: 'mixed' | 'values-first' | 'types-first' 'ignore-case'?: boolean 'ignore-alias'?: boolean } @@ -105,6 +106,16 @@ interface Options { - `asc` - enforce properties to be in ascending order. - `desc` - enforce properties to be in descending order. +### group-kind + +(default: `'mixed'`) + +Allows to group named imports by their kind, with value imports coming either before or after type imports. + +- `mixed` - does not group named imports by their kind +- `values-first` - groups all value imports before type imports +- `types-first` - groups all type imports before value imports + ### ignore-case (default: `false`) diff --git a/rules/sort-named-exports.ts b/rules/sort-named-exports.ts index 8bae878b1..297040aef 100644 --- a/rules/sort-named-exports.ts +++ b/rules/sort-named-exports.ts @@ -1,9 +1,10 @@ import type { SortingNode } from '../typings' import { createEslintRule } from '../utils/create-eslint-rule' +import { SortOrder, GroupKind, SortType } from '../typings' +import { getGroupNumber } from '../utils/get-group-number' 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' @@ -14,6 +15,7 @@ type MESSAGE_ID = 'unexpectedNamedExportsOrder' type Options = [ Partial<{ + 'group-kind': GroupKind 'ignore-case': boolean order: SortOrder type: SortType @@ -52,6 +54,15 @@ export default createEslintRule({ type: 'boolean', default: false, }, + 'group-kind': { + enum: [ + GroupKind.mixed, + GroupKind['values-first'], + GroupKind['types-first'], + ], + default: GroupKind.mixed, + type: 'string', + }, }, additionalProperties: false, }, @@ -74,16 +85,38 @@ export default createEslintRule({ type: SortType.alphabetical, 'ignore-case': false, order: SortOrder.asc, + 'group-kind': GroupKind.mixed, }) let nodes: SortingNode[] = node.specifiers.map(specifier => ({ size: rangeToDiff(specifier.range), name: specifier.local.name, node: specifier, + group: specifier.exportKind, })) + let shouldGroupByKind = options['group-kind'] !== GroupKind.mixed + let groupKindOrder = + options['group-kind'] === GroupKind['values-first'] + ? ['value', 'type'] + : ['type', 'value'] + pairwise(nodes, (left, right) => { - if (isPositive(compare(left, right, options))) { + let leftNum = getGroupNumber(groupKindOrder, left) + let rightNum = getGroupNumber(groupKindOrder, right) + + if ( + (shouldGroupByKind && leftNum > rightNum) || + ((!shouldGroupByKind || leftNum === rightNum) && + isPositive(compare(left, right, options))) + ) { + let sortedNodes = shouldGroupByKind + ? groupKindOrder + .map(group => nodes.filter(n => n.group === group)) + .map(groupedNodes => sortNodes(groupedNodes, options)) + .flat() + : sortNodes(nodes, options) + context.report({ messageId: 'unexpectedNamedExportsOrder', data: { @@ -92,12 +125,7 @@ export default createEslintRule({ }, node: right.node, fix: fixer => - makeFixes( - fixer, - nodes, - sortNodes(nodes, options), - context.sourceCode, - ), + makeFixes(fixer, nodes, sortedNodes, context.sourceCode), }) } }) diff --git a/rules/sort-named-imports.ts b/rules/sort-named-imports.ts index 12001f67d..6344199e0 100644 --- a/rules/sort-named-imports.ts +++ b/rules/sort-named-imports.ts @@ -1,9 +1,10 @@ import type { SortingNode } from '../typings' import { createEslintRule } from '../utils/create-eslint-rule' +import { SortOrder, GroupKind, SortType } from '../typings' +import { getGroupNumber } from '../utils/get-group-number' 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' @@ -15,6 +16,7 @@ type MESSAGE_ID = 'unexpectedNamedImportsOrder' type Options = [ Partial<{ 'ignore-alias': boolean + 'group-kind': GroupKind 'ignore-case': boolean order: SortOrder type: SortType @@ -57,6 +59,15 @@ export default createEslintRule({ type: 'boolean', default: false, }, + 'group-kind': { + enum: [ + GroupKind.mixed, + GroupKind['values-first'], + GroupKind['types-first'], + ], + default: GroupKind.mixed, + type: 'string', + }, }, additionalProperties: false, }, @@ -84,24 +95,50 @@ export default createEslintRule({ 'ignore-alias': true, 'ignore-case': false, order: SortOrder.asc, + 'group-kind': GroupKind.mixed, }) let nodes: SortingNode[] = specifiers.map(specifier => { + let group let { name } = specifier.local - if (options['ignore-alias'] && specifier.type === 'ImportSpecifier') { - ;({ name } = specifier.imported) + if (specifier.type === 'ImportSpecifier') { + if (options['ignore-alias']) { + ;({ name } = specifier.imported) + } + group = specifier.importKind } return { size: rangeToDiff(specifier.range), node: specifier, name, + group, } }) + let shouldGroupByKind = options['group-kind'] !== GroupKind.mixed + let groupKindOrder = + options['group-kind'] === GroupKind['values-first'] + ? ['value', 'type'] + : ['type', 'value'] + pairwise(nodes, (left, right) => { - if (isPositive(compare(left, right, options))) { + let leftNum = getGroupNumber(groupKindOrder, left) + let rightNum = getGroupNumber(groupKindOrder, right) + + if ( + (shouldGroupByKind && leftNum > rightNum) || + ((!shouldGroupByKind || leftNum === rightNum) && + isPositive(compare(left, right, options))) + ) { + let sortedNodes = shouldGroupByKind + ? groupKindOrder + .map(group => nodes.filter(n => n.group === group)) + .map(groupedNodes => sortNodes(groupedNodes, options)) + .flat() + : sortNodes(nodes, options) + context.report({ messageId: 'unexpectedNamedImportsOrder', data: { @@ -110,12 +147,7 @@ export default createEslintRule({ }, node: right.node, fix: fixer => - makeFixes( - fixer, - nodes, - sortNodes(nodes, options), - context.sourceCode, - ), + makeFixes(fixer, nodes, sortedNodes, context.sourceCode), }) } }) diff --git a/test/sort-named-exports.test.ts b/test/sort-named-exports.test.ts index 0a8ea3ca7..ce4385405 100644 --- a/test/sort-named-exports.test.ts +++ b/test/sort-named-exports.test.ts @@ -3,7 +3,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import rule, { RULE_NAME } from '../rules/sort-named-exports' -import { SortOrder, SortType } from '../typings' +import { GroupKind, SortOrder, SortType } from '../typings' describe(RULE_NAME, () => { RuleTester.describeSkip = describe.skip @@ -62,6 +62,110 @@ describe(RULE_NAME, () => { }, ], }) + + ruleTester.run( + `${RULE_NAME}: sorts named exports grouping by their kind`, + rule, + { + valid: [ + { + code: dedent` + export { Kenshin, type Sakabotou, Sanosuke, type Zanbato } + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + }, + { + code: dedent` + export { Kenshin, Sanosuke, type Sakabotou, type Zanbato } + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + }, + { + code: dedent` + export { type Sakabotou, type Zanbato, Kenshin, Sanosuke } + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + }, + ], + invalid: [ + { + code: dedent` + export { type Zanbato, Sanosuke, type Sakabotou, Kenshin } + `, + output: dedent` + export { Kenshin, type Sakabotou, Sanosuke, type Zanbato } + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + errors: [ + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Zanbato', + right: 'Sanosuke', + }, + }, + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Sanosuke', + right: 'Sakabotou', + }, + }, + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Sakabotou', + right: 'Kenshin', + }, + }, + ], + }, + { + code: dedent` + export { type Zanbato, Sanosuke, type Sakabotou, Kenshin } + `, + output: dedent` + export { Kenshin, Sanosuke, type Sakabotou, type Zanbato } + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + errors: [ + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Zanbato', + right: 'Sanosuke', + }, + }, + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Sakabotou', + right: 'Kenshin', + }, + }, + ], + }, + { + code: dedent` + export { type Zanbato, Sanosuke, type Sakabotou, Kenshin } + `, + output: dedent` + export { type Sakabotou, type Zanbato, Kenshin, Sanosuke } + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + errors: [ + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Sanosuke', + right: 'Sakabotou', + }, + }, + ], + }, + ], + }, + ) }) describe(`${RULE_NAME}: sorting by natural order`, () => { @@ -109,6 +213,110 @@ describe(RULE_NAME, () => { }, ], }) + + ruleTester.run( + `${RULE_NAME}: sorts named exports grouping by their kind`, + rule, + { + valid: [ + { + code: dedent` + export { Kenshin, type Sakabotou, Sanosuke, type Zanbato } + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + }, + { + code: dedent` + export { Kenshin, Sanosuke, type Sakabotou, type Zanbato } + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + }, + { + code: dedent` + export { type Sakabotou, type Zanbato, Kenshin, Sanosuke } + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + }, + ], + invalid: [ + { + code: dedent` + export { type Zanbato, Sanosuke, type Sakabotou, Kenshin } + `, + output: dedent` + export { Kenshin, type Sakabotou, Sanosuke, type Zanbato } + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + errors: [ + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Zanbato', + right: 'Sanosuke', + }, + }, + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Sanosuke', + right: 'Sakabotou', + }, + }, + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Sakabotou', + right: 'Kenshin', + }, + }, + ], + }, + { + code: dedent` + export { type Zanbato, Sanosuke, type Sakabotou, Kenshin } + `, + output: dedent` + export { Kenshin, Sanosuke, type Sakabotou, type Zanbato } + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + errors: [ + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Zanbato', + right: 'Sanosuke', + }, + }, + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Sakabotou', + right: 'Kenshin', + }, + }, + ], + }, + { + code: dedent` + export { type Zanbato, Sanosuke, type Sakabotou, Kenshin } + `, + output: dedent` + export { type Sakabotou, type Zanbato, Kenshin, Sanosuke } + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + errors: [ + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Sanosuke', + right: 'Sakabotou', + }, + }, + ], + }, + ], + }, + ) }) describe(`${RULE_NAME}: sorting by line length`, () => { @@ -155,6 +363,157 @@ describe(RULE_NAME, () => { }, ], }) + + ruleTester.run( + `${RULE_NAME}: sorts named exports grouping by their kind`, + rule, + { + valid: [ + { + code: dedent` + export { + Kaoru as Kamiya, + type Sakabotou, + type Zanbato, + Sanosuke, + Kenshin, + } + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + }, + { + code: dedent` + export { + Kaoru as Kamiya, + Sanosuke, + Kenshin, + type Sakabotou, + type Zanbato, + } + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + }, + { + code: dedent` + export { + type Sakabotou, + type Zanbato, + Kaoru as Kamiya, + Sanosuke, + Kenshin, + } + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + }, + ], + invalid: [ + { + code: dedent` + export { + Kaoru as Kamiya, + type Sakabotou, + Sanosuke, + type Zanbato, + Kenshin, + } + `, + output: dedent` + export { + Kaoru as Kamiya, + type Sakabotou, + type Zanbato, + Sanosuke, + Kenshin, + } + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + errors: [ + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Sanosuke', + right: 'Zanbato', + }, + }, + ], + }, + { + code: dedent` + export { + Kaoru as Kamiya, + type Sakabotou, + Sanosuke, + type Zanbato, + Kenshin, + } + `, + output: dedent` + export { + Kaoru as Kamiya, + Sanosuke, + Kenshin, + type Sakabotou, + type Zanbato, + } + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + errors: [ + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Sakabotou', + right: 'Sanosuke', + }, + }, + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Zanbato', + right: 'Kenshin', + }, + }, + ], + }, + { + code: dedent` + export { + Kaoru as Kamiya, + type Sakabotou, + Sanosuke, + type Zanbato, + Kenshin, + } + `, + output: dedent` + export { + type Sakabotou, + type Zanbato, + Kaoru as Kamiya, + Sanosuke, + Kenshin, + } + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + errors: [ + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Kaoru', + right: 'Sakabotou', + }, + }, + { + messageId: 'unexpectedNamedExportsOrder', + data: { + left: 'Sanosuke', + right: 'Zanbato', + }, + }, + ], + }, + ], + }, + ) }) describe(`${RULE_NAME}: misc`, () => { diff --git a/test/sort-named-imports.test.ts b/test/sort-named-imports.test.ts index 2064c98a7..84a28bb96 100644 --- a/test/sort-named-imports.test.ts +++ b/test/sort-named-imports.test.ts @@ -3,7 +3,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import rule, { RULE_NAME } from '../rules/sort-named-imports' -import { SortOrder, SortType } from '../typings' +import { GroupKind, SortOrder, SortType } from '../typings' describe(RULE_NAME, () => { RuleTester.describeSkip = describe.skip @@ -300,6 +300,110 @@ describe(RULE_NAME, () => { }, ], }) + + ruleTester.run( + `${RULE_NAME}: sorts named imports grouping by their kind`, + rule, + { + valid: [ + { + code: dedent` + import { Kenshin, type Sakabotou, Sanosuke, type Zanbato } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + }, + { + code: dedent` + import { Kenshin, Sanosuke, type Sakabotou, type Zanbato } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + }, + { + code: dedent` + import { type Sakabotou, type Zanbato, Kenshin, Sanosuke } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + }, + ], + invalid: [ + { + code: dedent` + import { type Zanbato, Sanosuke, type Sakabotou, Kenshin } from 'rurouni-kenshin' + `, + output: dedent` + import { Kenshin, type Sakabotou, Sanosuke, type Zanbato } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + errors: [ + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Zanbato', + right: 'Sanosuke', + }, + }, + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Sanosuke', + right: 'Sakabotou', + }, + }, + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Sakabotou', + right: 'Kenshin', + }, + }, + ], + }, + { + code: dedent` + import { type Zanbato, Sanosuke, type Sakabotou, Kenshin } from 'rurouni-kenshin' + `, + output: dedent` + import { Kenshin, Sanosuke, type Sakabotou, type Zanbato } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + errors: [ + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Zanbato', + right: 'Sanosuke', + }, + }, + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Sakabotou', + right: 'Kenshin', + }, + }, + ], + }, + { + code: dedent` + import { type Zanbato, Sanosuke, type Sakabotou, Kenshin } from 'rurouni-kenshin' + `, + output: dedent` + import { type Sakabotou, type Zanbato, Kenshin, Sanosuke } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + errors: [ + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Sanosuke', + right: 'Sakabotou', + }, + }, + ], + }, + ], + }, + ) }) describe(`${RULE_NAME}: sorting by natural order`, () => { @@ -585,6 +689,110 @@ describe(RULE_NAME, () => { }, ], }) + + ruleTester.run( + `${RULE_NAME}: sorts named imports grouping by their kind`, + rule, + { + valid: [ + { + code: dedent` + import { Kenshin, type Sakabotou, Sanosuke, type Zanbato } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + }, + { + code: dedent` + import { Kenshin, Sanosuke, type Sakabotou, type Zanbato } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + }, + { + code: dedent` + import { type Sakabotou, type Zanbato, Kenshin, Sanosuke } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + }, + ], + invalid: [ + { + code: dedent` + import { type Zanbato, Sanosuke, type Sakabotou, Kenshin } from 'rurouni-kenshin' + `, + output: dedent` + import { Kenshin, type Sakabotou, Sanosuke, type Zanbato } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + errors: [ + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Zanbato', + right: 'Sanosuke', + }, + }, + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Sanosuke', + right: 'Sakabotou', + }, + }, + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Sakabotou', + right: 'Kenshin', + }, + }, + ], + }, + { + code: dedent` + import { type Zanbato, Sanosuke, type Sakabotou, Kenshin } from 'rurouni-kenshin' + `, + output: dedent` + import { Kenshin, Sanosuke, type Sakabotou, type Zanbato } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + errors: [ + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Zanbato', + right: 'Sanosuke', + }, + }, + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Sakabotou', + right: 'Kenshin', + }, + }, + ], + }, + { + code: dedent` + import { type Zanbato, Sanosuke, type Sakabotou, Kenshin } from 'rurouni-kenshin' + `, + output: dedent` + import { type Sakabotou, type Zanbato, Kenshin, Sanosuke } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + errors: [ + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Sanosuke', + right: 'Sakabotou', + }, + }, + ], + }, + ], + }, + ) }) describe(`${RULE_NAME}: sorting by line length`, () => { @@ -809,6 +1017,157 @@ describe(RULE_NAME, () => { }, ], }) + + ruleTester.run( + `${RULE_NAME}: sorts named imports grouping by their kind`, + rule, + { + valid: [ + { + code: dedent` + import { + Kaoru as Kamiya, + type Sakabotou, + type Zanbato, + Sanosuke, + Kenshin, + } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + }, + { + code: dedent` + import { + Kaoru as Kamiya, + Sanosuke, + Kenshin, + type Sakabotou, + type Zanbato, + } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + }, + { + code: dedent` + import { + type Sakabotou, + type Zanbato, + Kaoru as Kamiya, + Sanosuke, + Kenshin, + } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + }, + ], + invalid: [ + { + code: dedent` + import { + Kaoru as Kamiya, + type Sakabotou, + Sanosuke, + type Zanbato, + Kenshin, + } from 'rurouni-kenshin' + `, + output: dedent` + import { + Kaoru as Kamiya, + type Sakabotou, + type Zanbato, + Sanosuke, + Kenshin, + } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind.mixed }], + errors: [ + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Sanosuke', + right: 'Zanbato', + }, + }, + ], + }, + { + code: dedent` + import { + Kaoru as Kamiya, + type Sakabotou, + Sanosuke, + type Zanbato, + Kenshin, + } from 'rurouni-kenshin' + `, + output: dedent` + import { + Kaoru as Kamiya, + Sanosuke, + Kenshin, + type Sakabotou, + type Zanbato, + } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['values-first'] }], + errors: [ + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Sakabotou', + right: 'Sanosuke', + }, + }, + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Zanbato', + right: 'Kenshin', + }, + }, + ], + }, + { + code: dedent` + import { + Kaoru as Kamiya, + type Sakabotou, + Sanosuke, + type Zanbato, + Kenshin, + } from 'rurouni-kenshin' + `, + output: dedent` + import { + type Sakabotou, + type Zanbato, + Kaoru as Kamiya, + Sanosuke, + Kenshin, + } from 'rurouni-kenshin' + `, + options: [{ ...options, 'group-kind': GroupKind['types-first'] }], + errors: [ + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Kamiya', + right: 'Sakabotou', + }, + }, + { + messageId: 'unexpectedNamedImportsOrder', + data: { + left: 'Sanosuke', + right: 'Zanbato', + }, + }, + ], + }, + ], + }, + ) }) describe(`${RULE_NAME}: misc`, () => { diff --git a/typings/index.ts b/typings/index.ts index ac2ae42c9..5787d2af0 100644 --- a/typings/index.ts +++ b/typings/index.ts @@ -11,6 +11,12 @@ export enum SortOrder { 'asc' = 'asc', } +export enum GroupKind { + 'values-first' = 'values-first', + 'types-first' = 'types-first', + 'mixed' = 'mixed', +} + export type PartitionComment = string[] | boolean | string export interface SortingNode {