diff --git a/rules/sort-classes-utils.ts b/rules/sort-classes-utils.ts index fcc0f5868..432bb0e4e 100644 --- a/rules/sort-classes-utils.ts +++ b/rules/sort-classes-utils.ts @@ -9,6 +9,8 @@ import type { } from './sort-classes.types' import type { CompareOptions } from '../utils/compare' +import { validateNoDuplicatedGroups } from '../utils/validate-groups-configuration' +import { allModifiers, allSelectors } from './sort-classes.types' import { matches } from '../utils/matches' interface CustomGroupMatchesProps { @@ -72,7 +74,7 @@ export const generateOfficialGroups = ( /** * Get possible combinations of n elements from an array */ -const getCombinations = (array: string[], n: number): string[][] => { +export const getCombinations = (array: string[], n: number): string[][] => { let result: string[][] = [] let backtrack = (start: number, comb: string[]) => { @@ -253,3 +255,47 @@ export const getCompareOptions = ( ignoreCase: options.ignoreCase, } } + +export let validateGroupsConfiguration = ( + groups: Required['groups'], + customGroups: Required['customGroups'], +): void => { + let availableCustomGroupNames = Array.isArray(customGroups) + ? customGroups.map(customGroup => customGroup.groupName) + : Object.keys(customGroups) + let invalidGroups = groups + .flat() + .filter( + group => + !isPredefinedGroup(group) && !availableCustomGroupNames.includes(group), + ) + if (invalidGroups.length) { + throw new Error('Invalid group(s): ' + invalidGroups.join(', ')) + } + validateNoDuplicatedGroups(groups) +} + +const isPredefinedGroup = (input: string): boolean => { + if (input === 'unknown') { + return true + } + let singleWordSelector = input.split('-').at(-1) + if (!singleWordSelector) { + return false + } + let twoWordsSelector = input.split('-').slice(-2).join('-') + let isTwoWordSelectorValid = allSelectors.includes( + twoWordsSelector as Selector, + ) + if ( + !allSelectors.includes(singleWordSelector as Selector) && + !isTwoWordSelectorValid + ) { + return false + } + let modifiers = input.split('-').slice(0, isTwoWordSelectorValid ? -2 : -1) + return ( + new Set(modifiers).size === modifiers.length && + modifiers.every(modifier => allModifiers.includes(modifier as Modifier)) + ) +} diff --git a/rules/sort-classes.ts b/rules/sort-classes.ts index 8ec4d3523..261c5db30 100644 --- a/rules/sort-classes.ts +++ b/rules/sort-classes.ts @@ -9,6 +9,7 @@ import type { import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies' import { + validateGroupsConfiguration, getOverloadSignatureGroups, generateOfficialGroups, customGroupMatches, @@ -243,6 +244,8 @@ export default createEslintRule({ order: 'asc', } as const) + validateGroupsConfiguration(options.groups, options.customGroups) + let sourceCode = getSourceCode(context) let className = node.parent.id?.name diff --git a/test/sort-classes-utils.test.ts b/test/sort-classes-utils.test.ts index e98fabf57..f879fadfb 100644 --- a/test/sort-classes-utils.test.ts +++ b/test/sort-classes-utils.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'vitest' -import { generateOfficialGroups } from '../rules/sort-classes-utils' +import { + validateGroupsConfiguration, + generateOfficialGroups, + getCombinations, +} from '../rules/sort-classes-utils' +import { allModifiers, allSelectors } from '../rules/sort-classes.types' describe('sort-classes-utils', () => { it('sort-classes-utils: should generate official groups', () => { @@ -44,4 +49,84 @@ describe('sort-classes-utils', () => { 'method', ]) }) + + describe('validateGroupsConfiguration', () => { + it('allows predefined groups', () => { + let allModifierCombinationPermutations = + getAllNonEmptyCombinations(allModifiers) + let allPredefinedGroups = allSelectors + .map(selector => + allModifierCombinationPermutations.map( + modifiers => `${modifiers.join('-')}-${selector}`, + ), + ) + .flat() + .concat(allSelectors) + expect( + validateGroupsConfiguration(allPredefinedGroups, []), + ).toBeUndefined() + }) + + it('allows custom groups with the new API', () => { + expect( + validateGroupsConfiguration( + ['static-property', 'myCustomGroup'], + [ + { + groupName: 'myCustomGroup', + }, + ], + ), + ).toBeUndefined() + }) + + it('throws an error with predefined groups with duplicate modifiers', () => { + expect(() => + validateGroupsConfiguration(['static-static-property'], []), + ).toThrow('Invalid group(s): static-static-property') + }) + + it('throws an error if a duplicate group is provided', () => { + expect(() => + validateGroupsConfiguration(['static-property', 'static-property'], []), + ).toThrow('Duplicated group(s): static-property') + }) + + it('throws an error if invalid groups are provided with the new API', () => { + expect(() => + validateGroupsConfiguration( + ['static-property', 'myCustomGroup', ''], + [ + { + groupName: 'myCustomGroupNotReferenced', + }, + ], + ), + ).toThrow('Invalid group(s): myCustomGroup') + }) + + it('allows groups with the old API', () => { + expect( + validateGroupsConfiguration(['static-property', 'myCustomGroup'], { + myCustomGroup: 'foo', + }), + ).toBeUndefined() + }) + + it('throws an error if invalid custom groups are provided with the old API', () => { + expect(() => + validateGroupsConfiguration(['static-property', 'myCustomGroup'], { + myCustomGroupNotReferenced: 'foo', + }), + ).toThrow('Invalid group(s): myCustomGroup') + }) + }) }) + +const getAllNonEmptyCombinations = (array: string[]): string[][] => { + let result: string[][] = [] + for (let i = 1; i < array.length; i++) { + result = [...result, ...getCombinations(array, i)] + } + return result +} diff --git a/test/validate-groups-configuration.test.ts b/test/validate-groups-configuration.test.ts index 22b43258a..7456e713a 100644 --- a/test/validate-groups-configuration.test.ts +++ b/test/validate-groups-configuration.test.ts @@ -21,4 +21,14 @@ describe('validate-groups-configuration', () => { ) }).toThrow('Invalid group(s): invalidGroup1, invalidGroup2') }) + + it('throws an error when a duplicate group is provided', () => { + expect(() => { + validateGroupsConfiguration( + ['predefinedGroup', 'predefinedGroup'], + ['predefinedGroup'], + [], + ) + }).toThrow('Duplicated group(s): predefinedGroup') + }) }) diff --git a/utils/validate-groups-configuration.ts b/utils/validate-groups-configuration.ts index a8d692b7a..60265dcde 100644 --- a/utils/validate-groups-configuration.ts +++ b/utils/validate-groups-configuration.ts @@ -1,3 +1,9 @@ +/** + * Throws an error if one of the following conditions is met: + * - One or more groups specified in `groups` are not predefined nor specified + * in `customGroups` + * - A group is specified in `groups` more than once + */ export let validateGroupsConfiguration = ( groups: (string[] | string)[], allowedPredefinedGroups: string[], @@ -13,4 +19,20 @@ export let validateGroupsConfiguration = ( if (invalidGroups.length) { throw new Error('Invalid group(s): ' + invalidGroups.join(', ')) } + validateNoDuplicatedGroups(groups) +} + +/** + * Throws an error if a group is specified more than once + */ +export let validateNoDuplicatedGroups = ( + groups: (string[] | string)[], +): void => { + let flattenGroups = groups.flat() + let duplicatedGroups = flattenGroups.filter( + (group, index) => flattenGroups.indexOf(group) !== index, + ) + if (duplicatedGroups.length) { + throw new Error('Duplicated group(s): ' + duplicatedGroups.join(', ')) + } }