diff --git a/rules/sort-astro-attributes.ts b/rules/sort-astro-attributes.ts index 814e0c7b..0629a6bf 100644 --- a/rules/sort-astro-attributes.ts +++ b/rules/sort-astro-attributes.ts @@ -5,6 +5,7 @@ import path from 'node:path' import type { SortingNode } from '../typings' +import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { createEslintRule } from '../utils/create-eslint-rule' import { getGroupNumber } from '../utils/get-group-number' import { getSourceCode } from '../utils/get-source-code' @@ -138,6 +139,12 @@ export default createEslintRule, MESSAGE_ID>({ groups: [], } as const) + validateGroupsConfiguration( + options.groups, + ['astro-shorthand', 'multiline', 'shorthand', 'unknown'], + Object.keys(options.customGroups), + ) + let sourceCode = getSourceCode(context) let parts: SortingNode[][] = attributes.reduce( diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts index 0472af62..fc9ace45 100644 --- a/rules/sort-imports.ts +++ b/rules/sort-imports.ts @@ -6,6 +6,7 @@ import { minimatch } from 'minimatch' import type { SortingNode } from '../typings' +import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { getCommentBefore } from '../utils/get-comment-before' import { createEslintRule } from '../utils/create-eslint-rule' import { getLinesBetween } from '../utils/get-lines-between' @@ -256,6 +257,34 @@ export default createEslintRule, MESSAGE_ID>({ options.groups = [...options.groups, 'unknown'] } + validateGroupsConfiguration( + options.groups, + [ + 'side-effect-style', + 'external-type', + 'internal-type', + 'builtin-type', + 'sibling-type', + 'parent-type', + 'side-effect', + 'index-type', + 'internal', + 'external', + 'sibling', + 'unknown', + 'builtin', + 'parent', + 'object', + 'index', + 'style', + 'type', + ], + [ + ...Object.keys(options.customGroups.type ?? {}), + ...Object.keys(options.customGroups.value ?? {}), + ], + ) + let nodes: SortingNode[] = [] let isSideEffectImport = (node: TSESTree.Node) => diff --git a/rules/sort-interfaces.ts b/rules/sort-interfaces.ts index 763e5ad6..3b91d34a 100644 --- a/rules/sort-interfaces.ts +++ b/rules/sort-interfaces.ts @@ -2,6 +2,7 @@ import { minimatch } from 'minimatch' import type { SortingNode } from '../typings' +import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { createEslintRule } from '../utils/create-eslint-rule' import { isMemberOptional } from '../utils/is-member-optional' import { getLinesBetween } from '../utils/get-lines-between' @@ -151,6 +152,12 @@ export default createEslintRule, MESSAGE_ID>({ groups: [], } as const) + validateGroupsConfiguration( + options.groups, + ['multiline', 'unknown'], + Object.keys(options.customGroups), + ) + let sourceCode = getSourceCode(context) if ( diff --git a/rules/sort-intersection-types.ts b/rules/sort-intersection-types.ts index aaabf476..12f4d8d6 100644 --- a/rules/sort-intersection-types.ts +++ b/rules/sort-intersection-types.ts @@ -1,5 +1,6 @@ import type { SortingNode } from '../typings' +import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { createEslintRule } from '../utils/create-eslint-rule' import { getGroupNumber } from '../utils/get-group-number' import { getSourceCode } from '../utils/get-source-code' @@ -113,6 +114,26 @@ export default createEslintRule({ groups: [], } as const) + validateGroupsConfiguration( + options.groups, + [ + 'intersection', + 'conditional', + 'function', + 'operator', + 'keyword', + 'literal', + 'nullish', + 'unknown', + 'import', + 'object', + 'named', + 'tuple', + 'union', + ], + [], + ) + let sourceCode = getSourceCode(context) let nodes: SortingNode[] = node.types.map(type => { diff --git a/rules/sort-jsx-props.ts b/rules/sort-jsx-props.ts index f8b6cc96..aaca0a13 100644 --- a/rules/sort-jsx-props.ts +++ b/rules/sort-jsx-props.ts @@ -4,6 +4,7 @@ import { minimatch } from 'minimatch' import type { SortingNode } from '../typings' +import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { createEslintRule } from '../utils/create-eslint-rule' import { getGroupNumber } from '../utils/get-group-number' import { getSourceCode } from '../utils/get-source-code' @@ -139,6 +140,12 @@ export default createEslintRule, MESSAGE_ID>({ groups: [], } as const) + validateGroupsConfiguration( + options.groups, + ['multiline', 'shorthand', 'unknown'], + Object.keys(options.customGroups), + ) + let sourceCode = getSourceCode(context) let shouldIgnore = false diff --git a/rules/sort-object-types.ts b/rules/sort-object-types.ts index 01f7fed2..7158ffb5 100644 --- a/rules/sort-object-types.ts +++ b/rules/sort-object-types.ts @@ -2,6 +2,7 @@ import type { TSESTree } from '@typescript-eslint/types' import type { SortingNode } from '../typings' +import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { createEslintRule } from '../utils/create-eslint-rule' import { getLinesBetween } from '../utils/get-lines-between' import { getGroupNumber } from '../utils/get-group-number' @@ -24,12 +25,12 @@ type Group = 'multiline' | 'unknown' | T[number] type Options = [ Partial<{ groupKind: 'required-first' | 'optional-first' | 'mixed' + customGroups: { [key in T[number]]: string[] | string } type: 'alphabetical' | 'line-length' | 'natural' groups: (Group[] | Group)[] partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean - customGroups: {} }>, ] @@ -140,6 +141,12 @@ export default createEslintRule, MESSAGE_ID>({ groups: [], } as const) + validateGroupsConfiguration( + options.groups, + ['multiline', 'unknown'], + Object.keys(options.customGroups), + ) + let sourceCode = getSourceCode(context) let formattedMembers: SortingNode[][] = diff --git a/rules/sort-objects.ts b/rules/sort-objects.ts index 0695a153..1e52e6e1 100644 --- a/rules/sort-objects.ts +++ b/rules/sort-objects.ts @@ -5,6 +5,7 @@ import { minimatch } from 'minimatch' import type { SortingNode } from '../typings' +import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { isPartitionComment } from '../utils/is-partition-comment' import { getCommentBefore } from '../utils/get-comment-before' import { createEslintRule } from '../utils/create-eslint-rule' @@ -30,6 +31,8 @@ export enum Position { 'ignore' = 'ignore', } +type Group = 'unknown' | string + type SortingNodeWithPosition = { position: Position } & SortingNode @@ -39,7 +42,7 @@ type Options = [ customGroups: { [key: string]: string[] | string } type: 'alphabetical' | 'line-length' | 'natural' partitionByComment: string[] | boolean | string - groups: (string[] | string)[] + groups: (Group[] | Group)[] partitionByNewLine: boolean styledComponents: boolean destructureOnly: boolean @@ -191,6 +194,12 @@ export default createEslintRule({ groups: [], } as const) + validateGroupsConfiguration( + options.groups, + ['unknown'], + Object.keys(options.customGroups), + ) + let shouldIgnore = false if (options.destructureOnly) { diff --git a/rules/sort-svelte-attributes.ts b/rules/sort-svelte-attributes.ts index d6f61e8d..9ab64a18 100644 --- a/rules/sort-svelte-attributes.ts +++ b/rules/sort-svelte-attributes.ts @@ -5,6 +5,7 @@ import path from 'node:path' import type { SortingNode } from '../typings' +import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { createEslintRule } from '../utils/create-eslint-rule' import { getGroupNumber } from '../utils/get-group-number' import { getSourceCode } from '../utils/get-source-code' @@ -135,6 +136,12 @@ export default createEslintRule, MESSAGE_ID>({ groups: [], } as const) + validateGroupsConfiguration( + options.groups, + ['svelte-shorthand', 'multiline', 'shorthand', 'unknown'], + Object.keys(options.customGroups), + ) + let sourceCode = getSourceCode(context) let parts: SortingNode[][] = node.attributes.reduce( diff --git a/rules/sort-union-types.ts b/rules/sort-union-types.ts index 1a4f2d52..a58fa337 100644 --- a/rules/sort-union-types.ts +++ b/rules/sort-union-types.ts @@ -1,5 +1,6 @@ import type { SortingNode } from '../typings' +import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { createEslintRule } from '../utils/create-eslint-rule' import { getGroupNumber } from '../utils/get-group-number' import { getSourceCode } from '../utils/get-source-code' @@ -113,6 +114,26 @@ export default createEslintRule({ groups: [], } as const) + validateGroupsConfiguration( + options.groups, + [ + 'intersection', + 'conditional', + 'function', + 'operator', + 'keyword', + 'literal', + 'nullish', + 'unknown', + 'import', + 'object', + 'named', + 'tuple', + 'union', + ], + [], + ) + let sourceCode = getSourceCode(context) let nodes: SortingNode[] = node.types.map(type => { diff --git a/rules/sort-vue-attributes.ts b/rules/sort-vue-attributes.ts index 72d81459..64405dfb 100644 --- a/rules/sort-vue-attributes.ts +++ b/rules/sort-vue-attributes.ts @@ -5,6 +5,7 @@ import path from 'node:path' import type { SortingNode } from '../typings' +import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { createEslintRule } from '../utils/create-eslint-rule' import { getGroupNumber } from '../utils/get-group-number' import { getSourceCode } from '../utils/get-source-code' @@ -147,6 +148,12 @@ export default createEslintRule, MESSAGE_ID>({ groups: [], } as const) + validateGroupsConfiguration( + options.groups, + ['multiline', 'shorthand', 'unknown'], + Object.keys(options.customGroups), + ) + let parts: SortingNode[][] = node.attributes.reduce( (accumulator: SortingNode[][], attribute) => { if ( diff --git a/test/sort-astro-attributes.test.ts b/test/sort-astro-attributes.test.ts index fc95938b..8dccc5e0 100644 --- a/test/sort-astro-attributes.test.ts +++ b/test/sort-astro-attributes.test.ts @@ -270,7 +270,7 @@ describe(ruleName, () => { options: [ { ...options, - groups: ['unknown', ['svelte-shorthand', 'shorthand']], + groups: ['unknown', ['shorthand']], }, ], errors: [ @@ -699,7 +699,7 @@ describe(ruleName, () => { options: [ { ...options, - groups: ['unknown', ['svelte-shorthand', 'shorthand']], + groups: ['unknown', ['shorthand']], }, ], errors: [ @@ -1130,7 +1130,7 @@ describe(ruleName, () => { options: [ { ...options, - groups: ['unknown', ['svelte-shorthand', 'shorthand']], + groups: ['unknown', ['shorthand']], }, ], errors: [ @@ -1333,6 +1333,41 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: validating group configuration`, () => { + ruleTester.run( + `${ruleName}: allows predefined groups and defined custom groups`, + rule, + { + valid: [ + { + filename: 'file.astro', + code: dedent` + --- + import Component from '../file2.astro' + --- + + `, + options: [ + { + groups: [ + 'astro-shorthand', + 'multiline', + 'shorthand', + 'unknown', + 'myCustomGroup', + ], + customGroups: { + myCustomGroup: 'x', + }, + }, + ], + }, + ], + invalid: [], + }, + ) + }) + describe(`${ruleName}: misc`, () => { ruleTester.run(`${ruleName}: works only for .astro files`, rule, { valid: [ diff --git a/test/sort-imports.test.ts b/test/sort-imports.test.ts index d0ef65a5..53132be4 100644 --- a/test/sort-imports.test.ts +++ b/test/sort-imports.test.ts @@ -4179,6 +4179,56 @@ describe(ruleName, () => { ) }) + describe(`${ruleName}: validating group configuration`, () => { + ruleTester.run( + `${ruleName}: allows predefined groups and defined custom groups`, + rule, + { + valid: [ + { + code: dedent` + import type { T } from 't' + + // @ts-expect-error missing types + import { t } from 't' + `, + options: [ + { + groups: [ + 'side-effect-style', + 'external-type', + 'internal-type', + 'builtin-type', + 'sibling-type', + 'parent-type', + 'side-effect', + 'index-type', + 'internal', + 'external', + 'sibling', + 'unknown', + 'builtin', + 'parent', + 'object', + 'index', + 'style', + 'type', + 'myCustomGroup1', + ], + customGroups: { + type: { + myCustomGroup1: 'x', + }, + }, + }, + ], + }, + ], + invalid: [], + }, + ) + }) + describe(`${ruleName}: misc`, () => { ruleTester.run( `${ruleName}: sets alphabetical asc sorting as default`, diff --git a/test/sort-interfaces.test.ts b/test/sort-interfaces.test.ts index c5f62424..0b94afb2 100644 --- a/test/sort-interfaces.test.ts +++ b/test/sort-interfaces.test.ts @@ -2203,6 +2203,35 @@ describe(ruleName, () => { ) }) + describe(`${ruleName}: validating group configuration`, () => { + ruleTester.run( + `${ruleName}: allows predefined groups and defined custom groups`, + rule, + { + valid: [ + { + code: dedent` + interface Interface { + a: string + b: 'b1' | 'b2', + c: string + } + `, + options: [ + { + groups: ['multiline', 'unknown', 'myCustomGroup'], + customGroups: { + myCustomGroup: 'x', + }, + }, + ], + }, + ], + invalid: [], + }, + ) + }) + describe(`${ruleName}: misc`, () => { ruleTester.run( `${ruleName}: sets alphabetical asc sorting as default`, diff --git a/test/sort-intersection-types.test.ts b/test/sort-intersection-types.test.ts index 604e6aea..729ee2e1 100644 --- a/test/sort-intersection-types.test.ts +++ b/test/sort-intersection-types.test.ts @@ -1144,6 +1144,38 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: validating group configuration`, () => { + ruleTester.run(`${ruleName}: allows predefined groups`, rule, { + valid: [ + { + code: dedent` + type Type = { label: 'aaa' } & { label: 'bb' } & { label: 'c' } + `, + options: [ + { + groups: [ + 'intersection', + 'conditional', + 'function', + 'operator', + 'keyword', + 'literal', + 'nullish', + 'unknown', + 'import', + 'object', + 'named', + 'tuple', + 'union', + ], + }, + ], + }, + ], + invalid: [], + }) + }) + describe(`${ruleName}: misc`, () => { ruleTester.run( `${ruleName}: sets alphabetical asc sorting as default`, diff --git a/test/sort-jsx-props.test.ts b/test/sort-jsx-props.test.ts index fa0d78d3..bcadf7b1 100644 --- a/test/sort-jsx-props.test.ts +++ b/test/sort-jsx-props.test.ts @@ -1408,6 +1408,39 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: validating group configuration`, () => { + ruleTester.run( + `${ruleName}: allows predefined groups and defined custom groups`, + rule, + { + valid: [ + { + code: dedent` + let Component = () => ( + + Value + + ) + `, + options: [ + { + groups: ['multiline', 'shorthand', 'unknown', 'myCustomGroup'], + customGroups: { + myCustomGroup: 'x', + }, + }, + ], + }, + ], + invalid: [], + }, + ) + }) + describe(`${ruleName}: misc`, () => { ruleTester.run( `${ruleName}: sets alphabetical asc sorting as default`, diff --git a/test/sort-object-types.test.ts b/test/sort-object-types.test.ts index bccbd1a3..3759f4b1 100644 --- a/test/sort-object-types.test.ts +++ b/test/sort-object-types.test.ts @@ -1740,6 +1740,35 @@ describe(ruleName, () => { ) }) + describe(`${ruleName}: validating group configuration`, () => { + ruleTester.run( + `${ruleName}: allows predefined groups and defined custom groups`, + rule, + { + valid: [ + { + code: dedent` + type Type = { + a: 'aaa' + b: 'bb' + c: 'c' + } + `, + options: [ + { + groups: ['multiline', 'unknown', 'myCustomGroup'], + customGroups: { + myCustomGroup: 'x', + }, + }, + ], + }, + ], + invalid: [], + }, + ) + }) + describe('misc', () => { ruleTester.run(`${ruleName}: ignores semi at the end of value`, rule, { valid: [ diff --git a/test/sort-svelte-attributes.test.ts b/test/sort-svelte-attributes.test.ts index 11f9a50f..cfd9e544 100644 --- a/test/sort-svelte-attributes.test.ts +++ b/test/sort-svelte-attributes.test.ts @@ -1340,6 +1340,42 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: validating group configuration`, () => { + ruleTester.run( + `${ruleName}: allows predefined groups and defined custom groups`, + rule, + { + valid: [ + { + filename: 'file.svelte', + code: dedent` + + + + `, + options: [ + { + groups: [ + 'svelte-shorthand', + 'multiline', + 'shorthand', + 'unknown', + 'myCustomGroup', + ], + customGroups: { + myCustomGroup: 'x', + }, + }, + ], + }, + ], + invalid: [], + }, + ) + }) + describe(`${ruleName}: misc`, () => { ruleTester.run(`${ruleName}: works only with .svelte files`, rule, { valid: [ diff --git a/test/sort-union-types.test.ts b/test/sort-union-types.test.ts index 92e29672..63b1649a 100644 --- a/test/sort-union-types.test.ts +++ b/test/sort-union-types.test.ts @@ -1176,6 +1176,38 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: validating group configuration`, () => { + ruleTester.run(`${ruleName}: allows predefined groups`, rule, { + valid: [ + { + code: dedent` + type Type = 'aaaa' | 'bbb' | 'cc' | 'd' + `, + options: [ + { + groups: [ + 'intersection', + 'conditional', + 'function', + 'operator', + 'keyword', + 'literal', + 'nullish', + 'unknown', + 'import', + 'object', + 'named', + 'tuple', + 'union', + ], + }, + ], + }, + ], + invalid: [], + }) + }) + describe(`${ruleName}: misc`, () => { ruleTester.run( `${ruleName}: sets alphabetical asc sorting as default`, diff --git a/test/sort-vue-attributes.test.ts b/test/sort-vue-attributes.test.ts index 1b30e25b..2fe7748b 100644 --- a/test/sort-vue-attributes.test.ts +++ b/test/sort-vue-attributes.test.ts @@ -857,6 +857,43 @@ describe(ruleName, () => { ) }) + describe(`${ruleName}: validating group configuration`, () => { + ruleTester.run( + `${ruleName}: allows predefined groups and defined custom groups`, + rule, + { + valid: [ + { + filename: 'file2.vue', + code: dedent` + + + + `, + options: [ + { + groups: ['multiline', 'shorthand', 'unknown', 'myCustomGroup'], + customGroups: { + myCustomGroup: 'x', + }, + }, + ], + }, + ], + invalid: [], + }, + ) + }) + describe(`${ruleName}: misc`, () => { ruleTester.run(`${ruleName}: works only with .vue files`, rule, { valid: [ diff --git a/test/validate-groups-configuration.test.ts b/test/validate-groups-configuration.test.ts new file mode 100644 index 00000000..22b43258 --- /dev/null +++ b/test/validate-groups-configuration.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest' + +import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' + +/** + * It is currently not possible to test rules that throw errors + * (https://github.com/eslint/eslint/issues/13434), so getting 100% code + * coverage is not possible only through ESLint's RuleTester as there is no way + * to catch the error thrown from `validateGroupsConfiguration`. We can get 100% + * coverage temporarily with this unit test until that feature is implemented in + * ESLint. + * + */ +describe('validate-groups-configuration', () => { + it('throws an error when an invalid group is provided', () => { + expect(() => { + validateGroupsConfiguration( + ['predefinedGroup', ['customGroup', 'invalidGroup1'], 'invalidGroup2'], + ['predefinedGroup'], + ['customGroup'], + ) + }).toThrow('Invalid group(s): invalidGroup1, invalidGroup2') + }) +}) diff --git a/utils/validate-groups-configuration.ts b/utils/validate-groups-configuration.ts new file mode 100644 index 00000000..a8d692b7 --- /dev/null +++ b/utils/validate-groups-configuration.ts @@ -0,0 +1,16 @@ +export let validateGroupsConfiguration = ( + groups: (string[] | string)[], + allowedPredefinedGroups: string[], + allowedCustomGroups: string[], +): void => { + let allowedGroupsSet = new Set([ + ...allowedPredefinedGroups, + ...allowedCustomGroups, + ]) + let invalidGroups = groups + .flat() + .filter(group => !allowedGroupsSet.has(group)) + if (invalidGroups.length) { + throw new Error('Invalid group(s): ' + invalidGroups.join(', ')) + } +}