Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion rules/sort-classes-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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[]) => {
Expand Down Expand Up @@ -253,3 +255,47 @@ export const getCompareOptions = (
ignoreCase: options.ignoreCase,
}
}

export let validateGroupsConfiguration = (
groups: Required<SortClassesOptions[0]>['groups'],
customGroups: Required<SortClassesOptions[0]>['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))
)
}
3 changes: 3 additions & 0 deletions rules/sort-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies'

import {
validateGroupsConfiguration,
getOverloadSignatureGroups,
generateOfficialGroups,
customGroupMatches,
Expand Down Expand Up @@ -243,6 +244,8 @@ export default createEslintRule<SortClassesOptions, MESSAGE_ID>({
order: 'asc',
} as const)

validateGroupsConfiguration(options.groups, options.customGroups)

let sourceCode = getSourceCode(context)
let className = node.parent.id?.name

Expand Down
87 changes: 86 additions & 1 deletion test/sort-classes-utils.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions test/validate-groups-configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})
22 changes: 22 additions & 0 deletions utils/validate-groups-configuration.ts
Original file line number Diff line number Diff line change
@@ -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[],
Expand All @@ -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(', '))
}
}