diff --git a/docs/content/rules/sort-array-includes.mdx b/docs/content/rules/sort-array-includes.mdx index c0a3f1679..57ae2d215 100644 --- a/docs/content/rules/sort-array-includes.mdx +++ b/docs/content/rules/sort-array-includes.mdx @@ -177,10 +177,12 @@ Specifies the sorting locales. See [String.prototype.localeCompare() - locales]( - `string` — A BCP 47 language tag (e.g. `'en'`, `'en-US'`, `'zh-CN'`). - `string[]` — An array of BCP 47 language tags. -### groupKind +### [DEPRECATED] groupKind <sub>default: `'literals-first'`</sub> +Use the [groups](#groups) option with the `literal` and `spread` selectors instead. Make sure to set this option to `mixed`. + Allows you to group array elements by their kind, determining whether spread values should come before or after literal values. - `mixed` — Do not group array elements by their kind; spread values are sorted together with literal values. @@ -228,6 +230,129 @@ if ([ Each group of elements (separated by empty lines) is treated independently, and the order within each group is preserved. +### useConfigurationIf + +<sub> + type: `{ allNamesMatchPattern?: string }` +</sub> +<sub>default: `{}`</sub> + +Allows you to specify filters to match a particular options configuration for a given object. + +The first matching options configuration will be used. If no configuration matches, the default options configuration will be used. + +- `allNamesMatchPattern` — A regexp pattern that all object keys must match. + +Example configuration: +```ts +{ + 'perfectionist/sort-array-includes': [ + 'error', + { + groups: ['r', 'g', 'b'], // Sort colors by RGB + customGroups: [ + { + elementNamePattern: '^r$', + groupName: 'r', + }, + { + elementNamePattern: '^g$', + groupName: 'g', + }, + { + elementNamePattern: '^b$', + groupName: 'b', + }, + ], + useConfigurationIf: { + allNamesMatchPattern: '^r|g|b$', + }, + }, + { + type: 'alphabetical' // Fallback configuration + } + ], +} +``` + +### groups + +<sub> + type: `Array<string | string[]>` +</sub> +<sub>default: `[]`</sub> + +Allows you to specify a list of groups for sorting. Groups help organize elements into categories. + +Each element will be assigned a single group specified in the `groups` option (or the `unknown` group if no match is found). +The order of items in the `groups` option determines how groups are ordered. + +Within a given group, members will be sorted according to the `type`, `order`, `ignoreCase`, etc. options. + +Individual groups can be combined together by placing them in an array. The order of groups in that array does not matter. +All members of the groups in the array will be sorted together as if they were part of a single group. + +Predefined groups are characterized by a selector. + +##### List of selectors + +- `literal`: Array elements that are not spread values. +- `spread`: Array elements that are spread values. + +### customGroups + +<sub> + type: `Array<CustomGroupDefinition | CustomGroupAnyOfDefinition>` +</sub> +<sub>default: `{}`</sub> + +You can define your own groups and use regexp pattern to match specific object type members. + +A custom group definition may follow one of the two following interfaces: + +```ts +interface CustomGroupDefinition { + groupName: string + type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted' + order?: 'asc' | 'desc' + selector?: string + elementNamePattern?: string +} + +``` +An array element will match a `CustomGroupDefinition` group if it matches all the filters of the custom group's definition. + +or: + +```ts +interface CustomGroupAnyOfDefinition { + groupName: string + type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted' + order?: 'asc' | 'desc' + anyOf: Array<{ + selector?: string + elementNamePattern?: string + }> +} +``` + +An array element will match a `CustomGroupAnyOfDefinition` group if it matches all the filters of at least one of the `anyOf` items. + +#### Attributes + +- `groupName`: The group's name, which needs to be put in the `groups` option. +- `selector`: Filter on the `selector` of the element. +- `elementNamePattern`: If entered, will check that the name of the element matches the pattern entered. +- `type`: Overrides the sort type for that custom group. `unsorted` will not sort the group. +- `order`: Overrides the sort order for that custom group + +#### Match importance + +The `customGroups` list is ordered: +The first custom group definition that matches an element will be used. + +Custom groups have a higher priority than any predefined group. + ## Usage <CodeTabs @@ -252,6 +377,9 @@ Each group of elements (separated by empty lines) is treated independently, and specialCharacters: 'keep', groupKind: 'literals-first', partitionByNewLine: false, + useConfigurationIf: {}, + groups: [], + customGroups: [], }, ], }, @@ -278,6 +406,9 @@ Each group of elements (separated by empty lines) is treated independently, and specialCharacters: 'keep', groupKind: 'literals-first', partitionByNewLine: false, + useConfigurationIf: {}, + groups: [], + customGroups: [], }, ], }, diff --git a/docs/content/rules/sort-interfaces.mdx b/docs/content/rules/sort-interfaces.mdx index 2778809bb..4947477ec 100644 --- a/docs/content/rules/sort-interfaces.mdx +++ b/docs/content/rules/sort-interfaces.mdx @@ -410,12 +410,53 @@ Current API: You can define your own groups and use regexp patterns to match specific interface members. -Each key of `customGroups` represents a group name which you can then use in the `groups` option. The value for each key can either be of type: -- `string` — An interface member's name matching the value will be marked as part of the group referenced by the key. -- `string[]` — An interface member's name matching any of the values of the array will be marked as part of the group referenced by the key. -The order of values in the array does not matter. +A custom group definition may follow one of the two following interfaces: -Custom group matching takes precedence over predefined group matching. +```ts +interface CustomGroupDefinition { + groupName: string + type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted' + order?: 'asc' | 'desc' + selector?: string + modifiers?: string[] + elementNamePattern?: string +} + +``` +An interface member will match a `CustomGroupDefinition` group if it matches all the filters of the custom group's definition. + +or: + +```ts +interface CustomGroupAnyOfDefinition { + groupName: string + type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted' + order?: 'asc' | 'desc' + anyOf: Array<{ + selector?: string + modifiers?: string[] + elementNamePattern?: string + }> +} +``` + +An interface member will match a `CustomGroupAnyOfDefinition` group if it matches all the filters of at least one of the `anyOf` items. + +#### Attributes + +- `groupName`: The group's name, which needs to be put in the `groups` option. +- `selector`: Filter on the `selector` of the element. +- `modifiers`: Filter on the `modifiers` of the element. (All the modifiers of the element must be present in that list) +- `elementNamePattern`: If entered, will check that the name of the element matches the pattern entered. +- `type`: Overrides the sort type for that custom group. `unsorted` will not sort the group. +- `order`: Overrides the sort order for that custom group + +#### Match importance + +The `customGroups` list is ordered: +The first custom group definition that matches an element will be used. + +Custom groups have a higher priority than any predefined group. #### Example diff --git a/docs/content/rules/sort-object-types.mdx b/docs/content/rules/sort-object-types.mdx index 6502ba459..4ee7dbcf6 100644 --- a/docs/content/rules/sort-object-types.mdx +++ b/docs/content/rules/sort-object-types.mdx @@ -375,12 +375,53 @@ Current API: You can define your own groups and use regexp pattern to match specific object type members. -Each key of `customGroups` represents a group name which you can then use in the `groups` option. The value for each key can either be of type: -- `string` — A type member's name matching the value will be marked as part of the group referenced by the key. -- `string[]` — A type member's name matching any of the values of the array will be marked as part of the group referenced by the key. -The order of values in the array does not matter. +A custom group definition may follow one of the two following interfaces: -Custom group matching takes precedence over predefined group matching. +```ts +interface CustomGroupDefinition { + groupName: string + type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted' + order?: 'asc' | 'desc' + selector?: string + modifiers?: string[] + elementNamePattern?: string +} + +``` +An object type will match a `CustomGroupDefinition` group if it matches all the filters of the custom group's definition. + +or: + +```ts +interface CustomGroupAnyOfDefinition { + groupName: string + type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted' + order?: 'asc' | 'desc' + anyOf: Array<{ + selector?: string + modifiers?: string[] + elementNamePattern?: string + }> +} +``` + +An object type will match a `CustomGroupAnyOfDefinition` group if it matches all the filters of at least one of the `anyOf` items. + +#### Attributes + +- `groupName`: The group's name, which needs to be put in the `groups` option. +- `selector`: Filter on the `selector` of the element. +- `modifiers`: Filter on the `modifiers` of the element. (All the modifiers of the element must be present in that list) +- `elementNamePattern`: If entered, will check that the name of the element matches the pattern entered. +- `type`: Overrides the sort type for that custom group. `unsorted` will not sort the group. +- `order`: Overrides the sort order for that custom group + +#### Match importance + +The `customGroups` list is ordered: +The first custom group definition that matches an element will be used. + +Custom groups have a higher priority than any predefined group. #### Example diff --git a/docs/content/rules/sort-objects.mdx b/docs/content/rules/sort-objects.mdx index 2a8248460..80048322b 100644 --- a/docs/content/rules/sort-objects.mdx +++ b/docs/content/rules/sort-objects.mdx @@ -320,9 +320,9 @@ Example configuration: { groups: ['r', 'g', 'b'], // Sort colors by RGB customGroups: { - r: 'r', - g: 'g', - b: 'b', + r: '^r$', + g: '^g$', + b: '^b$', }, useConfigurationIf: { allNamesMatchPattern: '^r|g|b$', diff --git a/docs/content/rules/sort-sets.mdx b/docs/content/rules/sort-sets.mdx index 99bc8d876..a986afa70 100644 --- a/docs/content/rules/sort-sets.mdx +++ b/docs/content/rules/sort-sets.mdx @@ -183,10 +183,12 @@ Specifies the sorting locales. See [String.prototype.localeCompare() - locales]( - `string` — A BCP 47 language tag (e.g. `'en'`, `'en-US'`, `'zh-CN'`). - `string[]` — An array of BCP 47 language tags. -### groupKind +### [DEPRECATED] groupKind <sub>default: `'literals-first'`</sub> +Use the [groups](#groups) option with the `literal` and `spread` selectors instead. Make sure to set this option to `mixed`. + Allows you to group set elements by their kind, determining whether spread values should come before or after literal values. - `mixed` — Do not group set elements by their kind; spread values are sorted together with literal values. @@ -232,6 +234,84 @@ let items = new Set([ Each group of elements (separated by empty lines) is treated independently, and the order within each group is preserved. +### groups + +<sub> + type: `Array<string | string[]>` +</sub> +<sub>default: `[]`</sub> + +Allows you to specify a list of groups for sorting. Groups help organize elements into categories. + +Each element will be assigned a single group specified in the `groups` option (or the `unknown` group if no match is found). +The order of items in the `groups` option determines how groups are ordered. + +Within a given group, members will be sorted according to the `type`, `order`, `ignoreCase`, etc. options. + +Individual groups can be combined together by placing them in an array. The order of groups in that array does not matter. +All members of the groups in the array will be sorted together as if they were part of a single group. + +Predefined groups are characterized by a selector. + +##### List of selectors + +- `literal`: Array elements that are not spread values. +- `spread`: Array elements that are spread values. + +### customGroups + +<sub> + type: `Array<CustomGroupDefinition | CustomGroupAnyOfDefinition>` +</sub> +<sub>default: `{}`</sub> + +You can define your own groups and use regexp pattern to match specific object type members. + +A custom group definition may follow one of the two following interfaces: + +```ts +interface CustomGroupDefinition { + groupName: string + type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted' + order?: 'asc' | 'desc' + selector?: string + elementNamePattern?: string +} + +``` +A set element will match a `CustomGroupDefinition` group if it matches all the filters of the custom group's definition. + +or: + +```ts +interface CustomGroupAnyOfDefinition { + groupName: string + type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted' + order?: 'asc' | 'desc' + anyOf: Array<{ + selector?: string + elementNamePattern?: string + }> +} +``` + +A set element will match a `CustomGroupAnyOfDefinition` group if it matches all the filters of at least one of the `anyOf` items. + +#### Attributes + +- `groupName`: The group's name, which needs to be put in the `groups` option. +- `selector`: Filter on the `selector` of the element. +- `elementNamePattern`: If entered, will check that the name of the element matches the pattern entered. +- `type`: Overrides the sort type for that custom group. `unsorted` will not sort the group. +- `order`: Overrides the sort order for that custom group + +#### Match importance + +The `customGroups` list is ordered: +The first custom group definition that matches an element will be used. + +Custom groups have a higher priority than any predefined group. + ## Usage <CodeTabs @@ -256,6 +336,9 @@ Each group of elements (separated by empty lines) is treated independently, and specialCharacters: 'keep', groupKind: 'literals-first', partitionByNewLine: false, + useConfigurationIf: {}, + groups: [], + customGroups: [], }, ], }, @@ -282,6 +365,9 @@ Each group of elements (separated by empty lines) is treated independently, and specialCharacters: 'keep', groupKind: 'literals-first', partitionByNewLine: false, + useConfigurationIf: {}, + groups: [], + customGroups: [], }, ], }, diff --git a/rules/sort-array-includes-utils.ts b/rules/sort-array-includes-utils.ts new file mode 100644 index 000000000..349e517de --- /dev/null +++ b/rules/sort-array-includes-utils.ts @@ -0,0 +1,50 @@ +import type { + SingleCustomGroup, + AnyOfCustomGroup, + Selector, +} from './sort-array-includes.types' + +import { matches } from '../utils/matches' + +interface CustomGroupMatchesProps { + customGroup: SingleCustomGroup | AnyOfCustomGroup + selectors: Selector[] + elementName: string +} + +/** + * Determines whether a custom group matches the given properties. + * @param {CustomGroupMatchesProps} props - The properties to compare with the + * custom group, including selectors, modifiers, and element name. + * @returns {boolean} `true` if the custom group matches the properties; + * otherwise, `false`. + */ +export let customGroupMatches = (props: CustomGroupMatchesProps): boolean => { + if ('anyOf' in props.customGroup) { + // At least one subgroup must match + return props.customGroup.anyOf.some(subgroup => + customGroupMatches({ ...props, customGroup: subgroup }), + ) + } + if ( + props.customGroup.selector && + !props.selectors.includes(props.customGroup.selector) + ) { + return false + } + + if ( + 'elementNamePattern' in props.customGroup && + props.customGroup.elementNamePattern + ) { + let matchesElementNamePattern: boolean = matches( + props.elementName, + props.customGroup.elementNamePattern, + ) + if (!matchesElementNamePattern) { + return false + } + } + + return true +} diff --git a/rules/sort-array-includes.ts b/rules/sort-array-includes.ts index b7dbb5751..0463ef419 100644 --- a/rules/sort-array-includes.ts +++ b/rules/sort-array-includes.ts @@ -1,93 +1,109 @@ import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema' import type { RuleContext } from '@typescript-eslint/utils/ts-eslint' import type { TSESTree } from '@typescript-eslint/types' +import type { TSESLint } from '@typescript-eslint/utils' +import type { Selector, Options } from './sort-array-includes.types' import type { SortingNode } from '../typings' import { + buildCustomGroupsArrayJsonSchema, partitionByCommentJsonSchema, + useConfigurationIfJsonSchema, + partitionByNewLineJsonSchema, specialCharactersJsonSchema, ignoreCaseJsonSchema, alphabetJsonSchema, localesJsonSchema, + groupsJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' +import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration' +import { getCustomGroupsCompareOptions } from '../utils/get-custom-groups-compare-options' +import { getMatchingContextOptions } from '../utils/get-matching-context-options' +import { generatePredefinedGroups } from '../utils/generate-predefined-groups' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' +import { singleCustomGroupJsonSchema } from './sort-array-includes.types' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' import { hasPartitionComment } from '../utils/is-partition-comment' +import { sortNodesByGroups } from '../utils/sort-nodes-by-groups' import { getCommentsBefore } from '../utils/get-comments-before' +import { customGroupMatches } from './sort-array-includes-utils' import { createEslintRule } from '../utils/create-eslint-rule' import { getLinesBetween } from '../utils/get-lines-between' +import { allSelectors } from './sort-array-includes.types' +import { getGroupNumber } from '../utils/get-group-number' import { getSourceCode } from '../utils/get-source-code' import { toSingleLine } from '../utils/to-single-line' import { rangeToDiff } from '../utils/range-to-diff' import { getSettings } from '../utils/get-settings' import { isSortable } from '../utils/is-sortable' -import { sortNodes } from '../utils/sort-nodes' import { makeFixes } from '../utils/make-fixes' +import { useGroups } from '../utils/use-groups' import { complete } from '../utils/complete' import { pairwise } from '../utils/pairwise' -export type Options = [ - Partial<{ - type: 'alphabetical' | 'line-length' | 'natural' | 'custom' - groupKind: 'literals-first' | 'spreads-first' | 'mixed' - partitionByComment: string[] | boolean | string - specialCharacters: 'remove' | 'trim' | 'keep' - locales: NonNullable<Intl.LocalesArgument> - partitionByNewLine: boolean - order: 'desc' | 'asc' - ignoreCase: boolean - alphabet: string - }>, -] +/** + * Cache computed groups by modifiers and selectors for performance + */ +let cachedGroupsByModifiersAndSelectors = new Map<string, string[]>() interface SortArrayIncludesSortingNode extends SortingNode<TSESTree.SpreadElement | TSESTree.Expression> { groupKind: 'literal' | 'spread' } -type MESSAGE_ID = 'unexpectedArrayIncludesOrder' +type MESSAGE_ID = + | 'unexpectedArrayIncludesGroupOrder' + | 'unexpectedArrayIncludesOrder' export let defaultOptions: Required<Options[0]> = { groupKind: 'literals-first', specialCharacters: 'keep', partitionByComment: false, partitionByNewLine: false, + useConfigurationIf: {}, type: 'alphabetical', ignoreCase: true, locales: 'en-US', + customGroups: [], alphabet: '', order: 'asc', + groups: [], } export let jsonSchema: JSONSchema4 = { - properties: { - partitionByComment: { - ...partitionByCommentJsonSchema, - description: - 'Allows you to use comments to separate the array members into logical groups.', + items: { + properties: { + partitionByComment: { + ...partitionByCommentJsonSchema, + description: + 'Allows you to use comments to separate the array members into logical groups.', + }, + groupKind: { + enum: ['mixed', 'literals-first', 'spreads-first'], + description: 'Specifies top-level groups.', + type: 'string', + }, + customGroups: buildCustomGroupsArrayJsonSchema({ + singleCustomGroupJsonSchema, + }), + partitionByNewLine: partitionByNewLineJsonSchema, + useConfigurationIf: useConfigurationIfJsonSchema, + specialCharacters: specialCharactersJsonSchema, + ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, + locales: localesJsonSchema, + groups: groupsJsonSchema, + order: orderJsonSchema, + type: typeJsonSchema, }, - groupKind: { - enum: ['mixed', 'literals-first', 'spreads-first'], - description: 'Specifies top-level groups.', - type: 'string', - }, - partitionByNewLine: { - description: - 'Allows to use spaces to separate the nodes into logical groups.', - type: 'boolean', - }, - specialCharacters: specialCharactersJsonSchema, - ignoreCase: ignoreCaseJsonSchema, - alphabet: alphabetJsonSchema, - locales: localesJsonSchema, - order: orderJsonSchema, - type: typeJsonSchema, + additionalProperties: false, + type: 'object', }, - additionalProperties: false, - type: 'object', + uniqueItems: true, + type: 'array', } export default createEslintRule<Options, MESSAGE_ID>({ @@ -103,21 +119,30 @@ export default createEslintRule<Options, MESSAGE_ID>({ node.object.type === 'ArrayExpression' ? node.object.elements : node.object.arguments - sortArray<MESSAGE_ID>(context, 'unexpectedArrayIncludesOrder', elements) + sortArray<MESSAGE_ID>({ + availableMessageIds: { + unexpectedGroupOrder: 'unexpectedArrayIncludesGroupOrder', + unexpectedOrder: 'unexpectedArrayIncludesOrder', + }, + elements, + context, + }) } }, }), meta: { + messages: { + unexpectedArrayIncludesGroupOrder: + 'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).', + unexpectedArrayIncludesOrder: + 'Expected "{{right}}" to come before "{{left}}".', + }, docs: { description: 'Enforce sorted arrays before include method.', url: 'https://perfectionist.dev/rules/sort-array-includes', recommended: true, }, - messages: { - unexpectedArrayIncludesOrder: - 'Expected "{{right}}" to come before "{{left}}".', - }, - schema: [jsonSchema], + schema: jsonSchema, type: 'suggestion', fixable: 'code', }, @@ -125,18 +150,38 @@ export default createEslintRule<Options, MESSAGE_ID>({ name: 'sort-array-includes', }) -export let sortArray = <MessageIds extends string>( - context: Readonly<RuleContext<MessageIds, Options>>, - messageId: MessageIds, - elements: (TSESTree.SpreadElement | TSESTree.Expression | null)[], -): void => { +export let sortArray = <MessageIds extends string>({ + availableMessageIds, + elements, + context, +}: { + availableMessageIds: { + unexpectedGroupOrder: MessageIds + unexpectedOrder: MessageIds + } + elements: (TSESTree.SpreadElement | TSESTree.Expression | null)[] + context: Readonly<RuleContext<MessageIds, Options>> +}): void => { if (!isSortable(elements)) { return } - let settings = getSettings(context.settings) - let options = complete(context.options.at(0), settings, defaultOptions) let sourceCode = getSourceCode(context) + let settings = getSettings(context.settings) + let matchedContextOptions = getMatchingContextOptions({ + nodeNames: elements + .filter(element => element !== null) + .map(element => getNodeName({ sourceCode, element })), + contextOptions: context.options, + }) + let options = complete(matchedContextOptions, settings, defaultOptions) + validateGeneratedGroupsConfiguration({ + customGroups: options.customGroups, + selectors: allSelectors, + groups: options.groups, + modifiers: [], + }) + let eslintDisabledLines = getEslintDisabledLines({ ruleName: context.id, sourceCode, @@ -150,17 +195,52 @@ export let sortArray = <MessageIds extends string>( return accumulator } - let lastSortingNode = accumulator.at(-1)?.at(-1) + let { defineGroup, getGroup } = useGroups(options) + let groupKind: 'literal' | 'spread' + let selector: Selector + if (element.type === 'SpreadElement') { + groupKind = 'spread' + selector = 'spread' + } else { + groupKind = 'literal' + selector = 'literal' + } + + for (let predefinedGroup of generatePredefinedGroups({ + cache: cachedGroupsByModifiersAndSelectors, + selectors: [selector], + modifiers: [], + })) { + defineGroup(predefinedGroup) + } + + let name = getNodeName({ sourceCode, element }) + for (let customGroup of options.customGroups) { + if ( + customGroupMatches({ + selectors: [selector], + elementName: name, + customGroup, + }) + ) { + defineGroup(customGroup.groupName, true) + // If the custom group is not referenced in the `groups` option, it will be ignored + if (getGroup() === customGroup.groupName) { + break + } + } + } + let sortingNode: SortArrayIncludesSortingNode = { - name: - element.type === 'Literal' - ? `${element.value}` - : sourceCode.getText(element), isEslintDisabled: isNodeEslintDisabled(element, eslintDisabledLines), - groupKind: element.type === 'SpreadElement' ? 'spread' : 'literal', + name: getNodeName({ sourceCode, element }), size: rangeToDiff(element, sourceCode), + group: getGroup(), node: element, + groupKind, } + + let lastSortingNode = accumulator.at(-1)?.at(-1) if ( (options.partitionByComment && hasPartitionComment( @@ -205,7 +285,11 @@ export let sortArray = <MessageIds extends string>( ignoreEslintDisabledNodes: boolean, ): SortArrayIncludesSortingNode[] => filteredGroupKindNodes.flatMap(groupedNodes => - sortNodes(groupedNodes, options, { ignoreEslintDisabledNodes }), + sortNodesByGroups(groupedNodes, options, { + getGroupCompareOptions: groupNumber => + getCustomGroupsCompareOptions(options, groupNumber), + ignoreEslintDisabledNodes, + }), ) let sortedNodes = sortNodesIgnoringEslintDisabledNodes(false) let sortedNodesExcludingEslintDisabled = @@ -214,6 +298,9 @@ export let sortArray = <MessageIds extends string>( pairwise(nodes, (left, right) => { let indexOfLeft = sortedNodes.indexOf(left) let indexOfRight = sortedNodes.indexOf(right) + let leftNumber = getGroupNumber(options.groups, left) + let rightNumber = getGroupNumber(options.groups, right) + let indexOfRightExcludingEslintDisabled = sortedNodesExcludingEslintDisabled.indexOf(right) if ( @@ -235,10 +322,24 @@ export let sortArray = <MessageIds extends string>( data: { right: toSingleLine(right.name), left: toSingleLine(left.name), + rightGroup: right.group, + leftGroup: left.group, }, + messageId: + leftNumber === rightNumber + ? availableMessageIds.unexpectedOrder + : availableMessageIds.unexpectedGroupOrder, node: right.node, - messageId, }) }) } } + +let getNodeName = ({ + sourceCode, + element, +}: { + element: TSESTree.SpreadElement | TSESTree.Expression + sourceCode: TSESLint.SourceCode +}): string => + element.type === 'Literal' ? `${element.value}` : sourceCode.getText(element) diff --git a/rules/sort-array-includes.types.ts b/rules/sort-array-includes.types.ts new file mode 100644 index 000000000..bdaa5d22a --- /dev/null +++ b/rules/sort-array-includes.types.ts @@ -0,0 +1,63 @@ +import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema' + +import { + buildCustomGroupSelectorJsonSchema, + elementNamePatternJsonSchema, +} from '../utils/common-json-schemas' + +export type Options = Partial<{ + useConfigurationIf: { + allNamesMatchPattern?: string + } + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' + /** + * @deprecated for {@link `groups`} + */ + groupKind: 'literals-first' | 'spreads-first' | 'mixed' + partitionByComment: string[] | boolean | string + specialCharacters: 'remove' | 'trim' | 'keep' + locales: NonNullable<Intl.LocalesArgument> + customGroups: CustomGroup[] + partitionByNewLine: boolean + groups: (Group[] | Group)[] + order: 'desc' | 'asc' + ignoreCase: boolean + alphabet: string +}>[] + +export interface SingleCustomGroup { + elementNamePattern?: string + selector?: Selector +} + +export interface AnyOfCustomGroup { + anyOf: SingleCustomGroup[] +} + +export type Selector = LiteralSelector | SpreadSelector + +type CustomGroup = ( + | { + order?: Options[0]['order'] + type?: Options[0]['type'] + } + | { + type?: 'unsorted' + } +) & + (SingleCustomGroup | AnyOfCustomGroup) & { + groupName: string + } + +type LiteralSelector = 'literal' + +type Group = 'unknown' | string + +type SpreadSelector = 'spread' + +export let allSelectors: Selector[] = ['literal', 'spread'] + +export let singleCustomGroupJsonSchema: Record<string, JSONSchema4> = { + selector: buildCustomGroupSelectorJsonSchema(allSelectors), + elementNamePattern: elementNamePatternJsonSchema, +} diff --git a/rules/sort-classes.types.ts b/rules/sort-classes.types.ts index adde1916f..55dd3ef26 100644 --- a/rules/sort-classes.types.ts +++ b/rules/sort-classes.types.ts @@ -22,6 +22,7 @@ export type SortClassesOptions = [ alphabet: string }>, ] + export type SingleCustomGroup = | AdvancedSingleCustomGroup<FunctionPropertySelector> | AdvancedSingleCustomGroup<AccessorPropertySelector> @@ -32,6 +33,7 @@ export type SingleCustomGroup = | BaseSingleCustomGroup<StaticBlockSelector> | BaseSingleCustomGroup<ConstructorSelector> | AdvancedSingleCustomGroup<MethodSelector> + export type Selector = | AccessorPropertySelector | FunctionPropertySelector @@ -42,18 +44,7 @@ export type Selector = | SetMethodSelector | PropertySelector | MethodSelector -export type CustomGroup = ( - | { - order?: SortClassesOptions[0]['order'] - type?: SortClassesOptions[0]['type'] - } - | { - type?: 'unsorted' - } -) & - (SingleCustomGroup | AnyOfCustomGroup) & { - groupName: string - } + export type Modifier = | PublicOrProtectedOrPrivateModifier | DecoratedModifier @@ -64,9 +55,11 @@ export type Modifier = | DeclareModifier | StaticModifier | AsyncModifier + export interface AnyOfCustomGroup { anyOf: SingleCustomGroup[] } + /** * Only used in code as well */ @@ -112,6 +105,7 @@ interface AllowedModifiersPerSelector { constructor: PublicOrProtectedOrPrivateModifier 'static-block': never } + /** * Some invalid combinations are still handled by this type, such as * - private abstract X @@ -130,43 +124,69 @@ type Group = | MethodGroup | 'unknown' | string + +type CustomGroup = ( + | { + order?: SortClassesOptions[0]['order'] + type?: SortClassesOptions[0]['type'] + } + | { + type?: 'unsorted' + } +) & + (SingleCustomGroup | AnyOfCustomGroup) & { + groupName: string + } + type NonDeclarePropertyGroup = `${PublicOrProtectedOrPrivateModifierPrefix}${StaticOrAbstractModifierPrefix}${OverrideModifierPrefix}${ReadonlyModifierPrefix}${DecoratedModifierPrefix}${OptionalModifierPrefix}${PropertySelector}` + type FunctionPropertyGroup = `${PublicOrProtectedOrPrivateModifierPrefix}${StaticModifierPrefix}${OverrideModifierPrefix}${ReadonlyModifierPrefix}${DecoratedModifierPrefix}${AsyncModifierPrefix}${FunctionPropertySelector}` + type MethodGroup = `${PublicOrProtectedOrPrivateModifierPrefix}${StaticOrAbstractModifierPrefix}${OverrideModifierPrefix}${DecoratedModifierPrefix}${AsyncModifierPrefix}${OptionalModifierPrefix}${MethodSelector}` + type DeclarePropertyGroup = `${DeclareModifierPrefix}${PublicOrProtectedOrPrivateModifierPrefix}${StaticOrAbstractModifierPrefix}${ReadonlyModifierPrefix}${OptionalModifierPrefix}${PropertySelector}` + type GetMethodOrSetMethodGroup = `${PublicOrProtectedOrPrivateModifierPrefix}${StaticOrAbstractModifierPrefix}${OverrideModifierPrefix}${DecoratedModifierPrefix}${GetMethodOrSetMethodSelector}` type AccessorPropertyGroup = `${PublicOrProtectedOrPrivateModifierPrefix}${StaticOrAbstractModifierPrefix}${OverrideModifierPrefix}${DecoratedModifierPrefix}${AccessorPropertySelector}` + type AdvancedSingleCustomGroup<T extends Selector> = { decoratorNamePattern?: string elementValuePattern?: string elementNamePattern?: string } & BaseSingleCustomGroup<T> + type PublicOrProtectedOrPrivateModifierPrefix = WithDashSuffixOrEmpty< ProtectedModifier | PrivateModifier | PublicModifier > + interface BaseSingleCustomGroup<T extends Selector> { modifiers?: AllowedModifiersPerSelector[T][] selector?: T } type IndexSignatureGroup = `${StaticModifierPrefix}${ReadonlyModifierPrefix}${IndexSignatureSelector}` + type PublicOrProtectedOrPrivateModifier = | ProtectedModifier | PrivateModifier | PublicModifier + type StaticOrAbstractModifierPrefix = WithDashSuffixOrEmpty< AbstractModifier | StaticModifier > + type ConstructorGroup = `${PublicOrProtectedOrPrivateModifierPrefix}${ConstructorSelector}` + type GetMethodOrSetMethodSelector = GetMethodSelector | SetMethodSelector + type DecoratedModifierPrefix = WithDashSuffixOrEmpty<DecoratedModifier> type OverrideModifierPrefix = WithDashSuffixOrEmpty<OverrideModifier> @@ -174,10 +194,15 @@ type OverrideModifierPrefix = WithDashSuffixOrEmpty<OverrideModifier> type OptionalModifierPrefix = WithDashSuffixOrEmpty<OptionalModifier> type ReadonlyModifierPrefix = WithDashSuffixOrEmpty<ReadonlyModifier> + type DeclareModifierPrefix = WithDashSuffixOrEmpty<DeclareModifier> + type StaticModifierPrefix = WithDashSuffixOrEmpty<StaticModifier> + type AsyncModifierPrefix = WithDashSuffixOrEmpty<AsyncModifier> + type WithDashSuffixOrEmpty<T extends string> = `${T}-` | '' + type FunctionPropertySelector = 'function-property' type AccessorPropertySelector = 'accessor-property' @@ -187,13 +212,21 @@ type StaticBlockGroup = `${StaticBlockSelector}` type IndexSignatureSelector = 'index-signature' type StaticBlockSelector = 'static-block' + type ConstructorSelector = 'constructor' + type GetMethodSelector = 'get-method' + type SetMethodSelector = 'set-method' + type ProtectedModifier = 'protected' + type DecoratedModifier = 'decorated' + type AbstractModifier = 'abstract' + type OverrideModifier = 'override' + type ReadonlyModifier = 'readonly' type OptionalModifier = 'optional' diff --git a/rules/sort-modules.types.ts b/rules/sort-modules.types.ts index 838c2ef55..cf73cc1aa 100644 --- a/rules/sort-modules.types.ts +++ b/rules/sort-modules.types.ts @@ -21,6 +21,7 @@ export type SortModulesOptions = [ alphabet: string }>, ] + export type SingleCustomGroup = ( | (DecoratorNamePatternFilterCustomGroup & BaseSingleCustomGroup<ClassSelector>) @@ -30,18 +31,7 @@ export type SingleCustomGroup = ( | BaseSingleCustomGroup<TypeSelector> ) & ElementNamePatternFilterCustomGroup -export type CustomGroup = ( - | { - order?: SortModulesOptions[0]['order'] - type?: SortModulesOptions[0]['type'] - } - | { - type?: 'unsorted' - } -) & - (SingleCustomGroup | AnyOfCustomGroup) & { - groupName: string - } + export type Selector = // | NamespaceSelector | InterfaceSelector @@ -50,12 +40,14 @@ export type Selector = | ClassSelector | TypeSelector | EnumSelector + export type Modifier = | DecoratedModifier | DeclareModifier | DefaultModifier | ExportModifier | AsyncModifier + export interface AnyOfCustomGroup { anyOf: SingleCustomGroup[] } @@ -72,6 +64,19 @@ interface AllowedModifiersPerSelector { enum: DeclareModifier | ExportModifier type: DeclareModifier | ExportModifier } + +type CustomGroup = ( + | { + order?: SortModulesOptions[0]['order'] + type?: SortModulesOptions[0]['type'] + } + | { + type?: 'unsorted' + } +) & + (SingleCustomGroup | AnyOfCustomGroup) & { + groupName: string + } /** * Only used in code, so I don't know if it's worth maintaining this. */ @@ -86,12 +91,16 @@ type Group = | TypeGroup | 'unknown' | string + type NonDefaultClassGroup = `${ExportModifierPrefix}${DeclareModifierPrefix}${DecoratedModifierPrefix}${ClassSelector}` + type DefaultFunctionGroup = `${ExportModifierPrefix}${DefaultModifierPrefix}${AsyncModifierPrefix}${FunctionSelector}` + type DefaultClassGroup = `${ExportModifierPrefix}${DefaultModifierPrefix}${DecoratedModifierPrefix}${ClassSelector}` + interface BaseSingleCustomGroup<T extends Selector> { modifiers?: AllowedModifiersPerSelector[T][] selector?: T @@ -102,12 +111,16 @@ type NonDefaultInterfaceGroup = type NonDefaultFunctionGroup = `${ExportModifierPrefix}${DeclareModifierPrefix}${FunctionSelector}` + type DefaultInterfaceGroup = `${ExportModifierPrefix}${DefaultModifierPrefix}${InterfaceSelector}` + type TypeGroup = `${ExportModifierPrefix}${DeclareModifierPrefix}${TypeSelector}` + type EnumGroup = `${ExportModifierPrefix}${DeclareModifierPrefix}${EnumSelector}` + interface DecoratorNamePatternFilterCustomGroup { decoratorNamePattern?: string } @@ -115,12 +128,19 @@ interface DecoratorNamePatternFilterCustomGroup { interface ElementNamePatternFilterCustomGroup { elementNamePattern?: string } + type DecoratedModifierPrefix = WithDashSuffixOrEmpty<DecoratedModifier> + type DeclareModifierPrefix = WithDashSuffixOrEmpty<DeclareModifier> + type DefaultModifierPrefix = WithDashSuffixOrEmpty<DefaultModifier> + type ExportModifierPrefix = WithDashSuffixOrEmpty<ExportModifier> + type AsyncModifierPrefix = WithDashSuffixOrEmpty<AsyncModifier> + type WithDashSuffixOrEmpty<T extends string> = `${T}-` | '' + type DecoratedModifier = 'decorated' type InterfaceSelector = 'interface' diff --git a/rules/sort-object-types.types.ts b/rules/sort-object-types.types.ts index 8ce16ddb6..e2414f1a3 100644 --- a/rules/sort-object-types.types.ts +++ b/rules/sort-object-types.types.ts @@ -36,19 +36,6 @@ export type SingleCustomGroup = ( ) & ElementNamePatternFilterCustomGroup -export type CustomGroup = ( - | { - order?: Options[0]['order'] - type?: Options[0]['type'] - } - | { - type?: 'unsorted' - } -) & - (SingleCustomGroup | AnyOfCustomGroup) & { - groupName: string - } - export type Selector = | IndexSignatureSelector | MultilineSelector @@ -73,6 +60,19 @@ interface AllowedModifiersPerSelector { 'index-signature': never } +type CustomGroup = ( + | { + order?: Options[0]['order'] + type?: Options[0]['type'] + } + | { + type?: 'unsorted' + } +) & + (SingleCustomGroup | AnyOfCustomGroup) & { + groupName: string + } + type IndexSignatureGroup = `${OptionalModifierPrefix | RequiredModifierPrefix}${MultilineModifierPrefix}${IndexSignatureSelector}` diff --git a/rules/sort-sets.ts b/rules/sort-sets.ts index c99e23826..cdfcd18e4 100644 --- a/rules/sort-sets.ts +++ b/rules/sort-sets.ts @@ -1,9 +1,9 @@ -import type { Options } from './sort-array-includes' +import type { Options } from './sort-array-includes.types' import { defaultOptions, jsonSchema, sortArray } from './sort-array-includes' import { createEslintRule } from '../utils/create-eslint-rule' -type MESSAGE_ID = 'unexpectedSetsOrder' +type MESSAGE_ID = 'unexpectedSetsGroupOrder' | 'unexpectedSetsOrder' export default createEslintRule<Options, MESSAGE_ID>({ create: context => ({ @@ -21,20 +21,29 @@ export default createEslintRule<Options, MESSAGE_ID>({ node.arguments[0].type === 'ArrayExpression' ? node.arguments[0].elements : node.arguments[0].arguments - sortArray<MESSAGE_ID>(context, 'unexpectedSetsOrder', elements) + sortArray<MESSAGE_ID>({ + availableMessageIds: { + unexpectedGroupOrder: 'unexpectedSetsGroupOrder', + unexpectedOrder: 'unexpectedSetsOrder', + }, + elements, + context, + }) } }, }), meta: { + messages: { + unexpectedSetsGroupOrder: + 'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).', + unexpectedSetsOrder: 'Expected "{{right}}" to come before "{{left}}".', + }, docs: { url: 'https://perfectionist.dev/rules/sort-sets', description: 'Enforce sorted sets.', recommended: true, }, - messages: { - unexpectedSetsOrder: 'Expected "{{right}}" to come before "{{left}}".', - }, - schema: [jsonSchema], + schema: jsonSchema, type: 'suggestion', fixable: 'code', }, diff --git a/test/sort-array-includes.test.ts b/test/sort-array-includes.test.ts index 0959acb1c..4aa82d86b 100644 --- a/test/sort-array-includes.test.ts +++ b/test/sort-array-includes.test.ts @@ -766,6 +766,447 @@ describe(ruleName, () => { valid: [], }, ) + + ruleTester.run( + `${ruleName}(${type}): allows to use predefined groups`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'spread', + leftGroup: 'literal', + right: '...b', + left: 'c', + }, + messageId: 'unexpectedArrayIncludesGroupOrder', + }, + ], + options: [ + { + ...options, + groups: ['spread', 'literal'], + groupKind: 'mixed', + }, + ], + output: dedent` + [ + ...b, + 'a', + 'c' + ].includes(value) + `, + code: dedent` + [ + 'c', + ...b, + 'a' + ].includes(value) + `, + }, + ], + valid: [], + }, + ) + + describe(`${ruleName}: custom groups`, () => { + ruleTester.run(`${ruleName}: filters on selector`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'literalElements', + selector: 'literal', + }, + ], + groups: ['literalElements', 'unknown'], + groupKind: 'mixed', + }, + ], + errors: [ + { + data: { + rightGroup: 'literalElements', + leftGroup: 'unknown', + left: '...b', + right: 'a', + }, + messageId: 'unexpectedArrayIncludesGroupOrder', + }, + ], + output: dedent` + [ + 'a', + ...b, + ].includes(value) + `, + code: dedent` + [ + ...b, + 'a', + ].includes(value) + `, + }, + ], + valid: [], + }) + + ruleTester.run(`${ruleName}: filters on elementNamePattern`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'literalsStartingWithHello', + elementNamePattern: 'hello*', + selector: 'literal', + }, + ], + groups: ['literalsStartingWithHello', 'unknown'], + groupKind: 'mixed', + }, + ], + errors: [ + { + data: { + rightGroup: 'literalsStartingWithHello', + right: 'helloLiteral', + leftGroup: 'unknown', + left: 'b', + }, + messageId: 'unexpectedArrayIncludesGroupOrder', + }, + ], + output: dedent` + [ + 'helloLiteral', + 'a', + 'b', + ].includes(value) + `, + code: dedent` + [ + 'a', + 'b', + 'helloLiteral', + ].includes(value) + `, + }, + ], + valid: [], + }) + + ruleTester.run( + `${ruleName}: sort custom groups by overriding 'type' and 'order'`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + right: 'bb', + left: 'a', + }, + messageId: 'unexpectedArrayIncludesOrder', + }, + { + data: { + right: 'ccc', + left: 'bb', + }, + messageId: 'unexpectedArrayIncludesOrder', + }, + { + data: { + right: 'dddd', + left: 'ccc', + }, + messageId: 'unexpectedArrayIncludesOrder', + }, + { + data: { + rightGroup: 'reversedLiteralsByLineLength', + leftGroup: 'unknown', + left: '...m', + right: 'eee', + }, + messageId: 'unexpectedArrayIncludesGroupOrder', + }, + ], + options: [ + { + customGroups: [ + { + groupName: 'reversedLiteralsByLineLength', + selector: 'literal', + type: 'line-length', + order: 'desc', + }, + ], + groups: ['reversedLiteralsByLineLength', 'unknown'], + type: 'alphabetical', + groupKind: 'mixed', + order: 'asc', + }, + ], + output: dedent` + [ + 'dddd', + 'ccc', + 'eee', + 'bb', + 'ff', + 'a', + 'g', + ...m, + ...o, + ...p, + ].includes(value) + `, + code: dedent` + [ + 'a', + 'bb', + 'ccc', + 'dddd', + ...m, + 'eee', + 'ff', + 'g', + ...o, + ...p, + ].includes(value) + `, + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}: does not sort custom groups with 'unsorted' type`, + rule, + { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'unsortedLiterals', + selector: 'literal', + type: 'unsorted', + }, + ], + groups: ['unsortedLiterals', 'unknown'], + groupKind: 'mixed', + }, + ], + errors: [ + { + data: { + rightGroup: 'unsortedLiterals', + leftGroup: 'unknown', + left: '...m', + right: 'c', + }, + messageId: 'unexpectedArrayIncludesGroupOrder', + }, + ], + output: dedent` + [ + 'b', + 'a', + 'd', + 'e', + 'c', + ...m, + ].includes(value) + `, + code: dedent` + [ + 'b', + 'a', + 'd', + 'e', + ...m, + 'c', + ].includes(value) + `, + }, + ], + valid: [], + }, + ) + + ruleTester.run(`${ruleName}: sort custom group blocks`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + anyOf: [ + { + elementNamePattern: 'foo|Foo', + selector: 'literal', + }, + { + elementNamePattern: 'foo|Foo', + selector: 'spread', + }, + ], + groupName: 'elementsIncludingFoo', + }, + ], + groups: ['elementsIncludingFoo', 'unknown'], + }, + ], + errors: [ + { + data: { + rightGroup: 'elementsIncludingFoo', + leftGroup: 'unknown', + right: '...foo', + left: 'a', + }, + messageId: 'unexpectedArrayIncludesGroupOrder', + }, + ], + output: dedent` + [ + '...foo', + 'cFoo', + 'a', + ].includes(value) + `, + code: dedent` + [ + 'a', + '...foo', + 'cFoo', + ].includes(value) + `, + }, + ], + valid: [], + }) + + ruleTester.run( + `${ruleName}: allows to use regex for element names in custom groups`, + rule, + { + valid: [ + { + options: [ + { + customGroups: [ + { + elementNamePattern: '^(?!.*Foo).*$', + groupName: 'elementsWithoutFoo', + }, + ], + groups: ['unknown', 'elementsWithoutFoo'], + type: 'alphabetical', + }, + ], + code: dedent` + [ + 'iHaveFooInMyName', + 'meTooIHaveFoo', + 'a', + 'b', + ].includes(value) + `, + }, + ], + invalid: [], + }, + ) + }) + + describe(`${ruleName}(${type}): allows to use 'useConfigurationIf'`, () => { + ruleTester.run( + `${ruleName}(${type}): allows to use 'allNamesMatchPattern'`, + rule, + { + invalid: [ + { + options: [ + { + ...options, + useConfigurationIf: { + allNamesMatchPattern: 'foo', + }, + }, + { + ...options, + customGroups: [ + { + elementNamePattern: '^r$', + groupName: 'r', + }, + { + elementNamePattern: '^g$', + groupName: 'g', + }, + { + elementNamePattern: '^b$', + groupName: 'b', + }, + ], + useConfigurationIf: { + allNamesMatchPattern: '^r|g|b$', + }, + groups: ['r', 'g', 'b'], + }, + ], + errors: [ + { + data: { + rightGroup: 'g', + leftGroup: 'b', + right: 'g', + left: 'b', + }, + messageId: 'unexpectedArrayIncludesGroupOrder', + }, + { + data: { + rightGroup: 'r', + leftGroup: 'g', + right: 'r', + left: 'g', + }, + messageId: 'unexpectedArrayIncludesGroupOrder', + }, + ], + output: dedent` + [ + 'r', + 'g', + 'b', + ].includes(value) + `, + code: dedent` + [ + 'b', + 'g', + 'r', + ].includes(value) + `, + }, + ], + valid: [], + }, + ) + }) }) describe(`${ruleName}: sorting by natural order`, () => { diff --git a/test/sort-sets.test.ts b/test/sort-sets.test.ts index 460f26e3e..6d9f66d36 100644 --- a/test/sort-sets.test.ts +++ b/test/sort-sets.test.ts @@ -713,6 +713,447 @@ describe(ruleName, () => { valid: [], }, ) + + ruleTester.run( + `${ruleName}(${type}): allows to use predefined groups`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'spread', + leftGroup: 'literal', + right: '...b', + left: 'c', + }, + messageId: 'unexpectedSetsGroupOrder', + }, + ], + options: [ + { + ...options, + groups: ['spread', 'literal'], + groupKind: 'mixed', + }, + ], + output: dedent` + new Set([ + ...b, + 'a', + 'c' + ]) + `, + code: dedent` + new Set([ + 'c', + ...b, + 'a' + ]) + `, + }, + ], + valid: [], + }, + ) + + describe(`${ruleName}: custom groups`, () => { + ruleTester.run(`${ruleName}: filters on selector`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'literalElements', + selector: 'literal', + }, + ], + groups: ['literalElements', 'unknown'], + groupKind: 'mixed', + }, + ], + errors: [ + { + data: { + rightGroup: 'literalElements', + leftGroup: 'unknown', + left: '...b', + right: 'a', + }, + messageId: 'unexpectedSetsGroupOrder', + }, + ], + output: dedent` + new Set([ + 'a', + ...b, + ]) + `, + code: dedent` + new Set([ + ...b, + 'a', + ]) + `, + }, + ], + valid: [], + }) + + ruleTester.run(`${ruleName}: filters on elementNamePattern`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'literalsStartingWithHello', + elementNamePattern: 'hello*', + selector: 'literal', + }, + ], + groups: ['literalsStartingWithHello', 'unknown'], + groupKind: 'mixed', + }, + ], + errors: [ + { + data: { + rightGroup: 'literalsStartingWithHello', + right: 'helloLiteral', + leftGroup: 'unknown', + left: 'b', + }, + messageId: 'unexpectedSetsGroupOrder', + }, + ], + output: dedent` + new Set([ + 'helloLiteral', + 'a', + 'b', + ]) + `, + code: dedent` + new Set([ + 'a', + 'b', + 'helloLiteral', + ]) + `, + }, + ], + valid: [], + }) + + ruleTester.run( + `${ruleName}: sort custom groups by overriding 'type' and 'order'`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + right: 'bb', + left: 'a', + }, + messageId: 'unexpectedSetsOrder', + }, + { + data: { + right: 'ccc', + left: 'bb', + }, + messageId: 'unexpectedSetsOrder', + }, + { + data: { + right: 'dddd', + left: 'ccc', + }, + messageId: 'unexpectedSetsOrder', + }, + { + data: { + rightGroup: 'reversedLiteralsByLineLength', + leftGroup: 'unknown', + left: '...m', + right: 'eee', + }, + messageId: 'unexpectedSetsGroupOrder', + }, + ], + options: [ + { + customGroups: [ + { + groupName: 'reversedLiteralsByLineLength', + selector: 'literal', + type: 'line-length', + order: 'desc', + }, + ], + groups: ['reversedLiteralsByLineLength', 'unknown'], + type: 'alphabetical', + groupKind: 'mixed', + order: 'asc', + }, + ], + output: dedent` + new Set([ + 'dddd', + 'ccc', + 'eee', + 'bb', + 'ff', + 'a', + 'g', + ...m, + ...o, + ...p, + ]) + `, + code: dedent` + new Set([ + 'a', + 'bb', + 'ccc', + 'dddd', + ...m, + 'eee', + 'ff', + 'g', + ...o, + ...p, + ]) + `, + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}: does not sort custom groups with 'unsorted' type`, + rule, + { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'unsortedLiterals', + selector: 'literal', + type: 'unsorted', + }, + ], + groups: ['unsortedLiterals', 'unknown'], + groupKind: 'mixed', + }, + ], + errors: [ + { + data: { + rightGroup: 'unsortedLiterals', + leftGroup: 'unknown', + left: '...m', + right: 'c', + }, + messageId: 'unexpectedSetsGroupOrder', + }, + ], + output: dedent` + new Set([ + 'b', + 'a', + 'd', + 'e', + 'c', + ...m, + ]) + `, + code: dedent` + new Set([ + 'b', + 'a', + 'd', + 'e', + ...m, + 'c', + ]) + `, + }, + ], + valid: [], + }, + ) + + ruleTester.run(`${ruleName}: sort custom group blocks`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + anyOf: [ + { + elementNamePattern: 'foo|Foo', + selector: 'literal', + }, + { + elementNamePattern: 'foo|Foo', + selector: 'spread', + }, + ], + groupName: 'elementsIncludingFoo', + }, + ], + groups: ['elementsIncludingFoo', 'unknown'], + }, + ], + errors: [ + { + data: { + rightGroup: 'elementsIncludingFoo', + leftGroup: 'unknown', + right: '...foo', + left: 'a', + }, + messageId: 'unexpectedSetsGroupOrder', + }, + ], + output: dedent` + new Set([ + '...foo', + 'cFoo', + 'a', + ]) + `, + code: dedent` + new Set([ + 'a', + '...foo', + 'cFoo', + ]) + `, + }, + ], + valid: [], + }) + + ruleTester.run( + `${ruleName}: allows to use regex for element names in custom groups`, + rule, + { + valid: [ + { + options: [ + { + customGroups: [ + { + elementNamePattern: '^(?!.*Foo).*$', + groupName: 'elementsWithoutFoo', + }, + ], + groups: ['unknown', 'elementsWithoutFoo'], + type: 'alphabetical', + }, + ], + code: dedent` + new Set([ + 'iHaveFooInMyName', + 'meTooIHaveFoo', + 'a', + 'b', + ]) + `, + }, + ], + invalid: [], + }, + ) + }) + + describe(`${ruleName}(${type}): allows to use 'useConfigurationIf'`, () => { + ruleTester.run( + `${ruleName}(${type}): allows to use 'allNamesMatchPattern'`, + rule, + { + invalid: [ + { + options: [ + { + ...options, + useConfigurationIf: { + allNamesMatchPattern: 'foo', + }, + }, + { + ...options, + customGroups: [ + { + elementNamePattern: '^r$', + groupName: 'r', + }, + { + elementNamePattern: '^g$', + groupName: 'g', + }, + { + elementNamePattern: '^b$', + groupName: 'b', + }, + ], + useConfigurationIf: { + allNamesMatchPattern: '^r|g|b$', + }, + groups: ['r', 'g', 'b'], + }, + ], + errors: [ + { + data: { + rightGroup: 'g', + leftGroup: 'b', + right: 'g', + left: 'b', + }, + messageId: 'unexpectedSetsGroupOrder', + }, + { + data: { + rightGroup: 'r', + leftGroup: 'g', + right: 'r', + left: 'g', + }, + messageId: 'unexpectedSetsGroupOrder', + }, + ], + output: dedent` + new Set([ + 'r', + 'g', + 'b', + ]) + `, + code: dedent` + new Set([ + 'b', + 'g', + 'r', + ]) + `, + }, + ], + valid: [], + }, + ) + }) }) describe(`${ruleName}: sorting by natural order`, () => {