diff --git a/docs/content/rules/sort-classes.mdx b/docs/content/rules/sort-classes.mdx index d9f33086..5065aae8 100644 --- a/docs/content/rules/sort-classes.mdx +++ b/docs/content/rules/sort-classes.mdx @@ -369,7 +369,6 @@ The `private` modifier will currently match any of the following: ##### Scope of the `public` modifier Elements that are not `protected` nor `private` will be matched with the `public` modifier, even if the keyword is not present. - ##### The `unknown` group Members that don’t fit into any group specified in the `groups` option will be placed in the `unknown` group. If the `unknown` group is not specified in the `groups` option, the members will remain in their original order. @@ -651,40 +650,38 @@ Example: 'unknown', ], + customGroups: [ // [!code ++] -+ [ // [!code ++] -+ { // [!code ++] -+ // `constructor()` members must not match // [!code ++] -+ // `unsorted-methods-and-other-properties` // [!code ++] -+ // so make them match this first // [!code ++] -+ groupName: 'constructor', // [!code ++] -+ selector: 'constructor', // [!code ++] -+ }, // [!code ++] -+ { // [!code ++] -+ groupName: 'input-properties', // [!code ++] -+ selector: 'property', // [!code ++] -+ modifiers: ['decorated'], // [!code ++] -+ decoratorNamePattern: 'Input', // [!code ++] -+ }, // [!code ++] -+ { // [!code ++] -+ groupName: 'output-properties', // [!code ++] -+ selector: 'property', // [!code ++] -+ modifiers: ['decorated'], // [!code ++] -+ decoratorNamePattern: 'Output', // [!code ++] -+ }, // [!code ++] -+ { // [!code ++] -+ groupName: 'unsorted-methods-and-other-properties', // [!code ++] -+ type: 'unsorted', // [!code ++] -+ anyOf: [ // [!code ++] -+ { // [!code ++] -+ selector: 'method', // [!code ++] -+ }, // [!code ++] -+ { // [!code ++] -+ selector: 'property', // [!code ++] -+ }, // [!code ++] -+ ] // [!code ++] -+ }, // [!code ++] -+ ] // [!code ++] -+ ] // [!code ++] ++ { // [!code ++] ++ // `constructor()` members must not match // [!code ++] ++ // `unsorted-methods-and-other-properties` // [!code ++] ++ // so make them match this first // [!code ++] ++ groupName: 'constructor', // [!code ++] ++ selector: 'constructor', // [!code ++] ++ }, // [!code ++] ++ { // [!code ++] ++ groupName: 'input-properties', // [!code ++] ++ selector: 'property', // [!code ++] ++ modifiers: ['decorated'], // [!code ++] ++ decoratorNamePattern: 'Input', // [!code ++] ++ }, // [!code ++] ++ { // [!code ++] ++ groupName: 'output-properties', // [!code ++] ++ selector: 'property', // [!code ++] ++ modifiers: ['decorated'], // [!code ++] ++ decoratorNamePattern: 'Output', // [!code ++] ++ }, // [!code ++] ++ { // [!code ++] ++ groupName: 'unsorted-methods-and-other-properties', // [!code ++] ++ type: 'unsorted', // [!code ++] ++ anyOf: [ // [!code ++] ++ { // [!code ++] ++ selector: 'method', // [!code ++] ++ }, // [!code ++] ++ { // [!code ++] ++ selector: 'property', // [!code ++] ++ }, // [!code ++] ++ ] // [!code ++] ++ }, // [!code ++] ++ ] // [!code ++] } ``` diff --git a/docs/content/rules/sort-interfaces.mdx b/docs/content/rules/sort-interfaces.mdx index bcaf765e..b62f3cbb 100644 --- a/docs/content/rules/sort-interfaces.mdx +++ b/docs/content/rules/sort-interfaces.mdx @@ -235,10 +235,12 @@ Specifies how new lines should be handled between interface groups. This options is only applicable when `partitionByNewLine` is `false`. -### groupKind +### [DEPRECATED] groupKind default: `'mixed'` +Use the [groups](#groups) option with the `optional` and `required` modifiers instead. + Specifies how optional and required members should be ordered in TypeScript interfaces. - `'optional-first'` — Put all optional members before required members. @@ -254,14 +256,6 @@ Specifies how optional and required members should be ordered in TypeScript inte Allows you to specify a list of interface member groups for sorting. Groups help organize members into categories, making your interfaces more readable and maintainable. -Predefined groups: - -- `'multiline'` — Members with multiline definitions, such as methods or properties with complex types. -- `'method'` - Members that are methods. -- `'unknown'` — Interface members that don’t fit into any group specified in the `groups` option. - -If the `unknown` group is not specified in the `groups` option, it will automatically be added to the end of the list. - Each interface member 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. @@ -270,6 +264,8 @@ Within a given group, members will be sorted according to the `type`, `order`, ` 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 single selector and potentially multiple modifiers. You may enter modifiers in any order, but the selector must always come at the end. + #### Example ```ts @@ -298,10 +294,106 @@ interface User { } ``` +#### Index-signatures + +- Selectors: `index-signature`, `member`. +- Modifiers: `required`, `optional`, `multiline`. +- Example: `optional-index-signature`, `index-signature`, `member`. + +#### Methods + +- Selectors: `method`, `member`. +- Modifiers: `required`, `optional`, `multiline`. +- Example: `optional-multiline-method`, `method`, `member`. + +#### Properties + +- Selectors: `property`, `member`. +- Modifiers: `required`, `optional`, `multiline`. +- Example: `optional-property`, `property`, `member`. + +##### Scope of the `required` modifier + +Elements that are not `optional` will be matched with the `required` modifier, even if the keyword is not present. + +##### The `unknown` group + +Members that don’t fit into any group specified in the `groups` option will be placed in the `unknown` group. If the `unknown` group is not specified in the `groups` option, +it will automatically be added to the end of the list. + +##### Behavior when multiple groups match an element + +The lists of modifiers above are sorted by importance, from most to least important. +In case of multiple groups matching an element, the following rules will be applied: + +1. The group with the most modifiers matching will be selected. +2. If modifiers quantity is the same, order will be chosen based on modifier importance as listed above. + +Example : + +```ts +interface Test { + optionalMethod?: () => { + property: string; + } +} +``` + +`optionalMethod` can be matched by the following groups, from most to least important: +- `multiline-optional-method` or `optional-multiline-method`. +- `multiline-method`. +- `optional-method`. +- `method`. +- `multiline-optional-member` or `optional-multiline-member`. +- `multiline-member`. +- `optional-member`. +- `member`. +- `unknown`. + +Example 2 (The most important group is written in the comments): + +```ts +interface Interface { + // 'index-signature' + [key: string]: any; + // 'optional-property' + description?: string; + // 'required-method' + method(): string +``` + ### customGroups + +Support for the object-based `customGroups` option is deprecated. + +Migrating from the old to the current API is easy: + +Old API: +```ts +{ + "key1": "value1", + "key2": "value2" +} +``` + +Current API: +```ts +[ + { + "groupName": "key1", + "elementNamePattern": "value1" + }, + { + "groupName": "key2", + "elementNamePattern": "value2" + } +] +``` + + - type: `{ [groupName: string]: string | string[] }` + type: `Array` default: `{}` @@ -316,7 +408,7 @@ Custom group matching takes precedence over predefined group matching. #### Example -Put all properties starting with `id` and `name` at the top, combine and sort metadata and multiline properties at the bottom. +Put all properties starting with `id` and `name` at the top, combine and sort metadata and optional multiline properties at the bottom. Anything else is put in the middle. ```ts @@ -326,7 +418,7 @@ interface User { age: number // unknown isAdmin: boolean // unknown lastUpdated_metadata: Date // bottom - localization: { // multiline + localization?: { // multiline // Stuff about localization } version_metadata: string // bottom @@ -338,14 +430,22 @@ interface User { ```js { groups: [ -+ 'top', // [!code ++] ++ 'top', // [!code ++] 'unknown', - ['multiline', 'bottom'] // [!code ++] ++ ['optional-multiline', 'bottom'] // [!code ++] ], -+ customGroups: { // [!code ++] -+ top: ['^id$', '^name$'] // [!code ++] -+ bottom: '.+_metadata$' // [!code ++] -+ } // [!code ++] ++ customGroups: [ // [!code ++] ++ { // [!code ++] ++ groupName: 'top', // [!code ++] ++ selector: 'property', // [!code ++] ++ elementNamePattern: '^(?:id|name)$', // [!code ++] ++ }, // [!code ++] ++ { // [!code ++] ++ groupName: 'bottom', // [!code ++] ++ selector: 'property', // [!code ++] ++ elementNamePattern: '.+_metadata$', // [!code ++] ++ } // [!code ++] ++ ] // [!code ++] } ``` diff --git a/docs/content/rules/sort-modules.mdx b/docs/content/rules/sort-modules.mdx index 9ec8db48..2a57d71e 100644 --- a/docs/content/rules/sort-modules.mdx +++ b/docs/content/rules/sort-modules.mdx @@ -366,7 +366,7 @@ Example : export default class {} ``` -`field` can be matched by the following groups, from most to least important: +`class` can be matched by the following groups, from most to least important: - `default-export-class` or `export-default-class`. - `default-class`. - `export-class`. @@ -446,40 +446,38 @@ Example: 'unknown', ], + customGroups: [ // [!code ++] -+ [ // [!code ++] -+ { // [!code ++] -+ groupName: 'input-types-and-interfaces', // [!code ++] -+ anyOf: [ // [!code ++] -+ { // [!code ++] -+ selector: 'type', // [!code ++] -+ elementNamePattern: 'Input'. // [!code ++] -+ }, // [!code ++] -+ { // [!code ++] -+ selector: 'interface', // [!code ++] -+ elementNamePattern: 'Input'. // [!code ++] -+ }, // [!code ++] -+ ] // [!code ++] -+ }, // [!code ++] -+ { // [!code ++] -+ groupName: 'output-types-and-interfaces', // [!code ++] -+ anyOf: [ // [!code ++] -+ { // [!code ++] -+ selector: 'type', // [!code ++] -+ elementNamePattern: 'Output' // [!code ++] -+ }, // [!code ++] -+ { // [!code ++] -+ selector: 'interface', // [!code ++] -+ elementNamePattern: 'Output' // [!code ++] -+ }, // [!code ++] -+ ] // [!code ++] -+ }, // [!code ++] -+ { // [!code ++] -+ groupName: 'unsorted-functions', // [!code ++] -+ type: 'unsorted', // [!code ++] -+ selector: 'function', // [!code ++] -+ }, // [!code ++] -+ ] // [!code ++] -+ ] // [!code ++] ++ { // [!code ++] ++ groupName: 'input-types-and-interfaces', // [!code ++] ++ anyOf: [ // [!code ++] ++ { // [!code ++] ++ selector: 'type', // [!code ++] ++ elementNamePattern: 'Input'. // [!code ++] ++ }, // [!code ++] ++ { // [!code ++] ++ selector: 'interface', // [!code ++] ++ elementNamePattern: 'Input'. // [!code ++] ++ }, // [!code ++] ++ ] // [!code ++] ++ }, // [!code ++] ++ { // [!code ++] ++ groupName: 'output-types-and-interfaces', // [!code ++] ++ anyOf: [ // [!code ++] ++ { // [!code ++] ++ selector: 'type', // [!code ++] ++ elementNamePattern: 'Output' // [!code ++] ++ }, // [!code ++] ++ { // [!code ++] ++ selector: 'interface', // [!code ++] ++ elementNamePattern: 'Output' // [!code ++] ++ }, // [!code ++] ++ ] // [!code ++] ++ }, // [!code ++] ++ { // [!code ++] ++ groupName: 'unsorted-functions', // [!code ++] ++ type: 'unsorted', // [!code ++] ++ selector: 'function', // [!code ++] ++ }, // [!code ++] ++ ] // [!code ++] } ``` diff --git a/docs/content/rules/sort-object-types.mdx b/docs/content/rules/sort-object-types.mdx index 30b00058..afce1585 100644 --- a/docs/content/rules/sort-object-types.mdx +++ b/docs/content/rules/sort-object-types.mdx @@ -142,6 +142,14 @@ 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. +### ignorePattern + +default: `[]` + +Allows you to specify names or patterns for object types that should be ignored by this rule. This can be useful if you have specific object types that you do not want to sort. + +You can specify their names or a regexp pattern to ignore, for example: `'^Component.+'` to ignore all object types whose names begin with the word “Component”. + ### partitionByComment default: `false` @@ -192,10 +200,12 @@ Specifies how new lines should be handled between object type groups. This options is only applicable when `partitionByNewLine` is `false`. -### groupKind +### [DEPRECATED] groupKind default: `'mixed'` +Use the [groups](#groups) option with the `optional` and `required` modifiers instead. + Allows you to group type object keys by their kind, determining whether required values should come before or after optional values. - `mixed` — Do not group object keys by their kind; required values are sorted together optional values. @@ -211,14 +221,6 @@ Allows you to group type object keys by their kind, determining whether required Allows you to specify a list of type properties groups for sorting. Groups help organize properties into categories, making your type definitions more readable and maintainable. -Predefined groups: - -- `'multiline'` — Properties with multiline definitions, such as methods or complex type declarations. -- `'method'` - Members that are methods. -- `'unknown'` — Properties that don’t fit into any group specified in the `groups` option. - -If the `unknown` group is not specified in the `groups` option, it will automatically be added to the end of the list. - Each property 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. @@ -227,6 +229,8 @@ Within a given group, members will be sorted according to the `type`, `order`, ` 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 single selector and potentially multiple modifiers. You may enter modifiers in any order, but the selector must always come at the end. + #### Example ```ts @@ -250,15 +254,111 @@ type User = { groups: [ 'unknown', 'method', - 'multiline', + 'multiline-member', ] } + +``` +#### Index-signatures + +- Selectors: `index-signature`, `member`. +- Modifiers: `required`, `optional`, `multiline`. +- Example: `optional-index-signature`, `index-signature`, `member`. + +#### Methods + +- Selectors: `method`, `member`. +- Modifiers: `required`, `optional`, `multiline`. +- Example: `optional-multiline-method`, `method`, `member`. + +#### Properties + +- Selectors: `property`, `member`. +- Modifiers: `required`, `optional`, `multiline`. +- Example: `optional-property`, `property`, `member`. + +##### Scope of the `required` modifier + +Elements that are not `optional` will be matched with the `required` modifier, even if the keyword is not present. + +##### The `unknown` group + +Members that don’t fit into any group specified in the `groups` option will be placed in the `unknown` group. If the `unknown` group is not specified in the `groups` option, +it will automatically be added to the end of the list. + +##### Behavior when multiple groups match an element + +The lists of modifiers above are sorted by importance, from most to least important. +In case of multiple groups matching an element, the following rules will be applied: + +1. The group with the most modifiers matching will be selected. +2. If modifiers quantity is the same, order will be chosen based on modifier importance as listed above. + +Example : + +```ts +interface Test { + optionalMethod?: () => { + property: string; + } +} +``` + +`optionalMethod` can be matched by the following groups, from most to least important: +- `multiline-optional-method` or `optional-multiline-method`. +- `multiline-method`. +- `optional-method`. +- `method`. +- `multiline-optional-member` or `optional-multiline-member`. +- `multiline-member`. +- `optional-member`. +- `member`. +- `unknown`. + +Example 2 (The most important group is written in the comments): + +```ts +interface Interface { + // 'index-signature' + [key: string]: any; + // 'optional-property' + description?: string; + // 'required-method' + method(): string ``` ### customGroups + +Support for the object-based `customGroups` option is deprecated. + +Migrating from the old to the current API is easy: + +Old API: +```ts +{ + "key1": "value1", + "key2": "value2" +} +``` + +Current API: +```ts +[ + { + "groupName": "key1", + "elementNamePattern": "value1" + }, + { + "groupName": "key2", + "elementNamePattern": "value2" + } +] +``` + + - type: `{ [groupName: string]: string | string[] }` + type: `Array` default: `{}` @@ -273,7 +373,7 @@ Custom group matching takes precedence over predefined group matching. #### Example -Put all properties starting with `id` and `name` at the top, combine and sort metadata and multiline properties at the bottom. +Put all properties starting with `id` and `name` at the top, combine and sort metadata and optional multiline properties at the bottom. Anything else is put in the middle. ```ts @@ -283,7 +383,7 @@ type User = { age: number // unknown isAdmin: boolean // unknown lastUpdated_metadata: Date // bottom - localization: { // multiline + localization?: { // multiline // Stuff about localization } version_metadata: string // bottom @@ -295,14 +395,22 @@ type User = { ```js { groups: [ -+ 'top', // [!code ++] ++ 'top', // [!code ++] 'unknown', - ['multiline', 'bottom'] // [!code ++] ++ ['optional-multiline', 'bottom'] // [!code ++] ], -+ customGroups: { // [!code ++] -+ top: ['^id$', '^name$'] // [!code ++] -+ bottom: '.+_metadata$' // [!code ++] -+ } // [!code ++] ++ customGroups: [ // [!code ++] ++ { // [!code ++] ++ groupName: 'top', // [!code ++] ++ selector: 'property', // [!code ++] ++ elementNamePattern: '^(?:id|name)$', // [!code ++] ++ }, // [!code ++] ++ { // [!code ++] ++ groupName: 'bottom', // [!code ++] ++ selector: 'property', // [!code ++] ++ elementNamePattern: '.+_metadata$', // [!code ++] ++ } // [!code ++] ++ ] // [!code ++] } ``` @@ -328,6 +436,7 @@ type User = { order: 'asc', ignoreCase: true, specialCharacters: 'keep', + ignorePattern: [], partitionByComment: false, partitionByNewLine: false, newlinesBetween: 'ignore', @@ -357,6 +466,7 @@ type User = { order: 'asc', ignoreCase: true, specialCharacters: 'keep', + ignorePattern: [], partitionByComment: false, partitionByNewLine: false, newlinesBetween: 'ignore', diff --git a/rules/get-custom-groups-compare-options.ts b/rules/get-custom-groups-compare-options.ts new file mode 100644 index 00000000..13ea9040 --- /dev/null +++ b/rules/get-custom-groups-compare-options.ts @@ -0,0 +1,64 @@ +import type { CompareOptions } from '../utils/compare' +import type { SortingNode } from '../typings' + +interface Options { + customGroups: Record | CustomGroup[] + type: 'alphabetical' | 'line-length' | 'natural' + specialCharacters: 'remove' | 'trim' | 'keep' + locales: NonNullable + groups: (string[] | string)[] + order: 'desc' | 'asc' + ignoreCase: boolean +} + +type CustomGroup = ( + | { + type?: 'alphabetical' | 'line-length' | 'natural' + order?: 'desc' | 'asc' + } + | { + type?: 'unsorted' + } +) & { + groupName: string +} + +/** + * Retrieves the compare options used to sort a given group. If the group is a + * custom group, its options will be favored over the default options. Returns + * `null` if the group should not be sorted. + * @param {Options} options - The sorting options, + * including groups and custom groups. + * @param {number} groupNumber - The index of the group to retrieve compare + * options for. + * @returns {CompareOptions | null} The compare options for the group, or `null` + * if the group should not be sorted. + */ +export let getCustomGroupsCompareOptions = ( + options: Required, + groupNumber: number, +): CompareOptions | null => { + if (!Array.isArray(options.customGroups)) { + return options + } + let group = options.groups[groupNumber] + let customGroup = + typeof group === 'string' + ? options.customGroups.find( + currentGroup => group === currentGroup.groupName, + ) + : null + if (customGroup?.type === 'unsorted') { + return null + } + return { + order: + customGroup && 'order' in customGroup && customGroup.order + ? customGroup.order + : options.order, + specialCharacters: options.specialCharacters, + type: customGroup?.type ?? options.type, + ignoreCase: options.ignoreCase, + locales: options.locales, + } +} diff --git a/rules/sort-classes-utils.ts b/rules/sort-classes-utils.ts index 3df9c7ff..58010cb0 100644 --- a/rules/sort-classes-utils.ts +++ b/rules/sort-classes-utils.ts @@ -1,14 +1,11 @@ import type { TSESTree } from '@typescript-eslint/utils' import type { - SortClassesOptions, SingleCustomGroup, AnyOfCustomGroup, Modifier, Selector, } from './sort-classes.types' -import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies' -import type { CompareOptions } from '../utils/compare' import { isSortable } from '../utils/is-sortable' import { matches } from '../utils/matches' @@ -136,40 +133,3 @@ export let customGroupMatches = (props: CustomGroupMatchesProps): boolean => { return true } - -/** - * Retrieves the compare options used to sort a given group. If the group is a - * custom group, its options will be favored over the default options. Returns - * `null` if the group should not be sorted. - * @param {Required[0]} options - The sorting options, - * including groups and custom groups. - * @param {number} groupNumber - The index of the group to retrieve compare - * options for. - * @returns {CompareOptions | null} The compare options for the group, or `null` - * if the group should not be sorted. - */ -export let getCompareOptions = ( - options: Required, - groupNumber: number, -): CompareOptions | null => { - let group = options.groups[groupNumber] - let customGroup = - typeof group === 'string' - ? options.customGroups.find( - currentGroup => group === currentGroup.groupName, - ) - : null - if (customGroup?.type === 'unsorted') { - return null - } - return { - order: - customGroup && 'order' in customGroup && customGroup.order - ? customGroup.order - : options.order, - specialCharacters: options.specialCharacters, - type: customGroup?.type ?? options.type, - ignoreCase: options.ignoreCase, - locales: options.locales, - } -} diff --git a/rules/sort-classes.ts b/rules/sort-classes.ts index 9559c4bc..31d15061 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 { + buildCustomGroupsArrayJsonSchema, partitionByCommentJsonSchema, partitionByNewLineJsonSchema, specialCharactersJsonSchema, @@ -19,24 +20,22 @@ import { orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' -import { - singleCustomGroupJsonSchema, - customGroupNameJsonSchema, - customGroupSortJsonSchema, - allModifiers, - allSelectors, -} from './sort-classes.types' import { getFirstUnorderedNodeDependentOn, sortNodesByDependencies, } from '../utils/sort-nodes-by-dependencies' import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration' +import { + singleCustomGroupJsonSchema, + allModifiers, + allSelectors, +} from './sort-classes.types' +import { validateGeneratedGroupsConfiguration } from './validate-generated-groups-configuration' import { getOverloadSignatureGroups, customGroupMatches, - getCompareOptions, } from './sort-classes-utils' -import { validateGeneratedGroupsConfiguration } from './validate-generated-groups-configuration' +import { getCustomGroupsCompareOptions } from './get-custom-groups-compare-options' import { generatePredefinedGroups } from '../utils/generate-predefined-groups' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' @@ -578,7 +577,7 @@ export default createEslintRule({ getGroupNumber(options.groups, sortingNode) === options.groups.length, getGroupCompareOptions: groupNumber => - getCompareOptions(options, groupNumber), + getCustomGroupsCompareOptions(options, groupNumber), ignoreEslintDisabledNodes, }), ), @@ -669,44 +668,6 @@ export default createEslintRule({ schema: [ { properties: { - customGroups: { - items: { - oneOf: [ - { - properties: { - ...customGroupNameJsonSchema, - ...customGroupSortJsonSchema, - anyOf: { - items: { - properties: { - ...singleCustomGroupJsonSchema, - }, - description: 'Custom group.', - additionalProperties: false, - type: 'object', - }, - type: 'array', - }, - }, - description: 'Custom group block.', - additionalProperties: false, - type: 'object', - }, - { - properties: { - ...customGroupNameJsonSchema, - ...customGroupSortJsonSchema, - ...singleCustomGroupJsonSchema, - }, - description: 'Custom group.', - additionalProperties: false, - type: 'object', - }, - ], - }, - description: 'Specifies custom groups.', - type: 'array', - }, ignoreCallbackDependenciesPatterns: { description: 'Patterns that should be ignored when detecting dependencies in method callbacks.', @@ -720,6 +681,9 @@ export default createEslintRule({ description: 'Allows to use comments to separate the class members into logical groups.', }, + customGroups: buildCustomGroupsArrayJsonSchema({ + singleCustomGroupJsonSchema, + }), partitionByNewLine: partitionByNewLineJsonSchema, specialCharacters: specialCharactersJsonSchema, newlinesBetween: newlinesBetweenJsonSchema, diff --git a/rules/sort-classes.types.ts b/rules/sort-classes.types.ts index 3fb9c095..754865c5 100644 --- a/rules/sort-classes.types.ts +++ b/rules/sort-classes.types.ts @@ -1,5 +1,11 @@ import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema' +import { + buildCustomGroupModifiersJsonSchema, + buildCustomGroupSelectorJsonSchema, + elementNamePatternJsonSchema, +} from '../utils/common-json-schemas' + export type SortClassesOptions = [ Partial<{ type: 'alphabetical' | 'line-length' | 'natural' @@ -231,39 +237,11 @@ export let allModifiers: Modifier[] = [ 'optional', ] -export let customGroupSortJsonSchema: Record = { - type: { - enum: ['alphabetical', 'line-length', 'natural', 'unsorted'], - description: 'Custom group sort type.', - type: 'string', - }, - order: { - description: 'Custom group sort order.', - enum: ['desc', 'asc'], - type: 'string', - }, -} - -export let customGroupNameJsonSchema: Record = { - groupName: { - description: 'Custom group name.', - type: 'string', - }, -} - /** * Ideally, we should generate as many schemas as there are selectors, and ensure * that users do not enter invalid modifiers for a given selector */ export let singleCustomGroupJsonSchema: Record = { - modifiers: { - items: { - enum: allModifiers, - type: 'string', - }, - description: 'Modifier filters.', - type: 'array', - }, elementValuePattern: { description: 'Element value pattern filter for properties.', type: 'string', @@ -272,13 +250,7 @@ export let singleCustomGroupJsonSchema: Record = { description: 'Decorator name pattern filter.', type: 'string', }, - selector: { - description: 'Selector filter.', - enum: allSelectors, - type: 'string', - }, - elementNamePattern: { - description: 'Element name pattern filter.', - type: 'string', - }, + modifiers: buildCustomGroupModifiersJsonSchema(allModifiers), + selector: buildCustomGroupSelectorJsonSchema(allSelectors), + elementNamePattern: elementNamePatternJsonSchema, } diff --git a/rules/sort-interfaces.ts b/rules/sort-interfaces.ts index fdaec2fa..5eb6bf10 100644 --- a/rules/sort-interfaces.ts +++ b/rules/sort-interfaces.ts @@ -1,58 +1,9 @@ -import type { TSESTree } from '@typescript-eslint/types' +import type { Options as SortObjectTypesOptions } from './sort-object-types.types' -import type { SortingNode } from '../typings' - -import { - partitionByCommentJsonSchema, - partitionByNewLineJsonSchema, - specialCharactersJsonSchema, - newlinesBetweenJsonSchema, - customGroupsJsonSchema, - ignoreCaseJsonSchema, - localesJsonSchema, - groupsJsonSchema, - orderJsonSchema, - typeJsonSchema, -} from '../utils/common-json-schemas' -import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration' -import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' -import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' -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 { makeNewlinesFixes } from '../utils/make-newlines-fixes' -import { getNewlinesErrors } from '../utils/get-newlines-errors' +import { sortObjectTypeElements, jsonSchema } from './sort-object-types' import { createEslintRule } from '../utils/create-eslint-rule' -import { isMemberOptional } from '../utils/is-member-optional' -import { getLinesBetween } from '../utils/get-lines-between' -import { getGroupNumber } from '../utils/get-group-number' -import { getSourceCode } from '../utils/get-source-code' -import { rangeToDiff } from '../utils/range-to-diff' -import { getSettings } from '../utils/get-settings' -import { isSortable } from '../utils/is-sortable' -import { useGroups } from '../utils/use-groups' -import { makeFixes } from '../utils/make-fixes' -import { complete } from '../utils/complete' -import { pairwise } from '../utils/pairwise' -import { matches } from '../utils/matches' -export type Options = [ - Partial<{ - groupKind: 'optional-first' | 'required-first' | 'mixed' - type: 'alphabetical' | 'line-length' | 'natural' - customGroups: Record - partitionByComment: string[] | boolean | string - newlinesBetween: 'ignore' | 'always' | 'never' - specialCharacters: 'remove' | 'trim' | 'keep' - locales: NonNullable - groups: (Group[] | Group)[] - partitionByNewLine: boolean - ignorePattern: string[] - order: 'desc' | 'asc' - ignoreCase: boolean - }>, -] +export type Options = SortObjectTypesOptions type MESSAGE_ID = | 'unexpectedInterfacePropertiesGroupOrder' @@ -60,13 +11,7 @@ type MESSAGE_ID = | 'extraSpacingBetweenInterfaceMembers' | 'unexpectedInterfacePropertiesOrder' -interface SortInterfacesSortingNode extends SortingNode { - groupKind: 'required' | 'optional' -} - -type Group = 'multiline' | 'unknown' | T[number] | 'method' - -let defaultOptions: Required[0]> = { +let defaultOptions: Required = { partitionByComment: false, partitionByNewLine: false, newlinesBetween: 'ignore', @@ -81,252 +26,8 @@ let defaultOptions: Required[0]> = { groups: [], } -export default createEslintRule, MESSAGE_ID>({ - create: context => ({ - TSInterfaceDeclaration: node => { - if (!isSortable(node.body.body)) { - return - } - - let settings = getSettings(context.settings) - let options = complete(context.options.at(0), settings, defaultOptions) - validateGroupsConfiguration( - options.groups, - ['multiline', 'method', 'unknown'], - Object.keys(options.customGroups), - ) - validateNewlinesAndPartitionConfiguration(options) - - if ( - options.ignorePattern.some(pattern => matches(node.id.name, pattern)) - ) { - return - } - - let sourceCode = getSourceCode(context) - let eslintDisabledLines = getEslintDisabledLines({ - ruleName: context.id, - sourceCode, - }) - - let formattedMembers: SortInterfacesSortingNode[][] = - node.body.body.reduce( - (accumulator: SortInterfacesSortingNode[][], element) => { - if (element.type === 'TSCallSignatureDeclaration') { - accumulator.push([]) - return accumulator - } - - let lastElement = accumulator.at(-1)?.at(-1) - let name: string - - let { setCustomGroups, defineGroup, getGroup } = useGroups(options) - - if (element.type === 'TSPropertySignature') { - if (element.key.type === 'Identifier') { - ;({ name } = element.key) - } else if (element.key.type === 'Literal') { - name = `${element.key.value}` - } else { - let end: number = - element.typeAnnotation?.range.at(0) ?? - element.range.at(1)! - (element.optional ? '?'.length : 0) - - name = sourceCode.text.slice(element.range.at(0), end) - } - } else if (element.type === 'TSIndexSignature') { - let endIndex: number = - element.typeAnnotation?.range.at(0) ?? element.range.at(1)! - - name = sourceCode.text.slice(element.range.at(0), endIndex) - } else { - let endIndex: number = - element.returnType?.range.at(0) ?? element.range.at(1)! - - name = sourceCode.text.slice(element.range.at(0), endIndex) - } - - setCustomGroups(options.customGroups, name) - - if ( - element.type === 'TSMethodSignature' || - (element.type === 'TSPropertySignature' && - element.typeAnnotation?.typeAnnotation.type === - 'TSFunctionType') - ) { - defineGroup('method') - } - - if (element.loc.start.line !== element.loc.end.line) { - defineGroup('multiline') - } - - let elementSortingNode: SortInterfacesSortingNode = { - isEslintDisabled: isNodeEslintDisabled( - element, - eslintDisabledLines, - ), - groupKind: isMemberOptional(element) ? 'optional' : 'required', - size: rangeToDiff(element, sourceCode), - addSafetySemicolonWhenInline: true, - group: getGroup(), - node: element, - name, - } - - if ( - (options.partitionByComment && - hasPartitionComment( - options.partitionByComment, - getCommentsBefore({ - node: element, - sourceCode, - }), - )) || - (options.partitionByNewLine && - lastElement && - getLinesBetween(sourceCode, lastElement, elementSortingNode)) - ) { - accumulator.push([]) - } - - accumulator.at(-1)!.push(elementSortingNode) - - return accumulator - }, - [[]], - ) - let groupKindOrder - if (options.groupKind === 'required-first') { - groupKindOrder = ['required', 'optional'] as const - } else if (options.groupKind === 'optional-first') { - groupKindOrder = ['optional', 'required'] as const - } else { - groupKindOrder = ['any'] as const - } - for (let nodes of formattedMembers) { - let filteredGroupKindNodes = groupKindOrder.map(groupKind => - nodes.filter( - currentNode => - groupKind === 'any' || currentNode.groupKind === groupKind, - ), - ) - let sortNodesExcludingEslintDisabled = ( - ignoreEslintDisabledNodes: boolean, - ): SortInterfacesSortingNode[] => - filteredGroupKindNodes.flatMap(groupedNodes => - sortNodesByGroups(groupedNodes, options, { - ignoreEslintDisabledNodes, - }), - ) - let sortedNodes = sortNodesExcludingEslintDisabled(false) - let sortedNodesExcludingEslintDisabled = - sortNodesExcludingEslintDisabled(true) - - pairwise(nodes, (left, right) => { - let leftNumber = getGroupNumber(options.groups, left) - let rightNumber = getGroupNumber(options.groups, right) - - let indexOfLeft = sortedNodes.indexOf(left) - let indexOfRight = sortedNodes.indexOf(right) - let indexOfRightExcludingEslintDisabled = - sortedNodesExcludingEslintDisabled.indexOf(right) - - let messageIds: MESSAGE_ID[] = [] - - if ( - indexOfLeft > indexOfRight || - indexOfLeft >= indexOfRightExcludingEslintDisabled - ) { - messageIds.push( - leftNumber === rightNumber - ? 'unexpectedInterfacePropertiesOrder' - : 'unexpectedInterfacePropertiesGroupOrder', - ) - } - - messageIds = [ - ...messageIds, - ...getNewlinesErrors({ - missedSpacingError: 'missedSpacingBetweenInterfaceMembers', - extraSpacingError: 'extraSpacingBetweenInterfaceMembers', - rightNum: rightNumber, - leftNum: leftNumber, - sourceCode, - options, - right, - left, - }), - ] - - for (let messageId of messageIds) { - context.report({ - fix: fixer => [ - ...makeFixes({ - sortedNodes: sortedNodesExcludingEslintDisabled, - sourceCode, - options, - fixer, - nodes, - }), - ...makeNewlinesFixes({ - sortedNodes: sortedNodesExcludingEslintDisabled, - sourceCode, - options, - fixer, - nodes, - }), - ], - data: { - rightGroup: right.group, - leftGroup: left.group, - right: right.name, - left: left.name, - }, - node: right.node, - messageId, - }) - } - }) - } - }, - }), +export default createEslintRule({ meta: { - schema: [ - { - properties: { - ignorePattern: { - description: - 'Specifies names or patterns for nodes that should be ignored by rule.', - items: { - type: 'string', - }, - type: 'array', - }, - partitionByComment: { - ...partitionByCommentJsonSchema, - description: - 'Allows you to use comments to separate the interface properties into logical groups.', - }, - groupKind: { - description: 'Specifies the order of optional and required nodes.', - enum: ['mixed', 'optional-first', 'required-first'], - type: 'string', - }, - partitionByNewLine: partitionByNewLineJsonSchema, - specialCharacters: specialCharactersJsonSchema, - newlinesBetween: newlinesBetweenJsonSchema, - customGroups: customGroupsJsonSchema, - ignoreCase: ignoreCaseJsonSchema, - locales: localesJsonSchema, - groups: groupsJsonSchema, - order: orderJsonSchema, - type: typeJsonSchema, - }, - additionalProperties: false, - type: 'object', - }, - ], messages: { unexpectedInterfacePropertiesGroupOrder: 'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).', @@ -342,9 +43,24 @@ export default createEslintRule, MESSAGE_ID>({ description: 'Enforce sorted interface properties.', recommended: true, }, + schema: [jsonSchema], type: 'suggestion', fixable: 'code', }, + create: context => ({ + TSInterfaceDeclaration: node => + sortObjectTypeElements({ + availableMessageIds: { + missedSpacingBetweenMembers: 'missedSpacingBetweenInterfaceMembers', + extraSpacingBetweenMembers: 'extraSpacingBetweenInterfaceMembers', + unexpectedGroupOrder: 'unexpectedInterfacePropertiesGroupOrder', + unexpectedOrder: 'unexpectedInterfacePropertiesOrder', + }, + parentNodeName: node.id.name, + elements: node.body.body, + context, + }), + }), defaultOptions: [defaultOptions], name: 'sort-interfaces', }) diff --git a/rules/sort-modules-utils.ts b/rules/sort-modules-utils.ts index a0a22cfa..ed45f4a0 100644 --- a/rules/sort-modules-utils.ts +++ b/rules/sort-modules-utils.ts @@ -1,12 +1,9 @@ import type { - SortModulesOptions, SingleCustomGroup, AnyOfCustomGroup, Modifier, Selector, } from './sort-modules.types' -import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies' -import type { CompareOptions } from '../utils/compare' import { matches } from '../utils/matches' @@ -75,40 +72,3 @@ export let customGroupMatches = (props: CustomGroupMatchesProps): boolean => { return true } - -/** - * Retrieves the compare options used to sort a given group. If the group is a - * custom group, its options will be favored over the default options. Returns - * `null` if the group should not be sorted. - * @param {Required[0]} options - The sorting options, - * including groups and custom groups. - * @param {number} groupNumber - The index of the group to retrieve compare - * options for. - * @returns {CompareOptions | null} The compare options for the group, or `null` - * if the group should not be sorted. - */ -export let getCompareOptions = ( - options: Required, - groupNumber: number, -): CompareOptions | null => { - let group = options.groups[groupNumber] - let customGroup = - typeof group === 'string' - ? options.customGroups.find( - currentGroup => group === currentGroup.groupName, - ) - : null - if (customGroup?.type === 'unsorted') { - return null - } - return { - order: - customGroup && 'order' in customGroup && customGroup.order - ? customGroup.order - : options.order, - specialCharacters: options.specialCharacters, - type: customGroup?.type ?? options.type, - ignoreCase: options.ignoreCase, - locales: options.locales, - } -} diff --git a/rules/sort-modules.ts b/rules/sort-modules.ts index 798e3852..31501abf 100644 --- a/rules/sort-modules.ts +++ b/rules/sort-modules.ts @@ -11,6 +11,7 @@ import type { import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies' import { + buildCustomGroupsArrayJsonSchema, partitionByCommentJsonSchema, partitionByNewLineJsonSchema, specialCharactersJsonSchema, @@ -21,21 +22,19 @@ import { orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' -import { - singleCustomGroupJsonSchema, - customGroupNameJsonSchema, - customGroupSortJsonSchema, - allModifiers, - allSelectors, -} from './sort-modules.types' import { getFirstUnorderedNodeDependentOn, sortNodesByDependencies, } from '../utils/sort-nodes-by-dependencies' import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration' +import { + singleCustomGroupJsonSchema, + allModifiers, + allSelectors, +} from './sort-modules.types' import { validateGeneratedGroupsConfiguration } from './validate-generated-groups-configuration' +import { getCustomGroupsCompareOptions } from './get-custom-groups-compare-options' import { generatePredefinedGroups } from '../utils/generate-predefined-groups' -import { customGroupMatches, getCompareOptions } from './sort-modules-utils' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' import { hasPartitionComment } from '../utils/is-partition-comment' @@ -48,6 +47,7 @@ import { createEslintRule } from '../utils/create-eslint-rule' import { getLinesBetween } from '../utils/get-lines-between' import { getGroupNumber } from '../utils/get-group-number' import { getEnumMembers } from '../utils/get-enum-members' +import { customGroupMatches } from './sort-modules-utils' import { getSourceCode } from '../utils/get-source-code' import { toSingleLine } from '../utils/to-single-line' import { rangeToDiff } from '../utils/range-to-diff' @@ -101,49 +101,14 @@ export default createEslintRule({ schema: [ { properties: { - customGroups: { - items: { - oneOf: [ - { - properties: { - ...customGroupNameJsonSchema, - ...customGroupSortJsonSchema, - anyOf: { - items: { - properties: { - ...singleCustomGroupJsonSchema, - }, - description: 'Custom group.', - additionalProperties: false, - type: 'object', - }, - type: 'array', - }, - }, - description: 'Custom group block.', - additionalProperties: false, - type: 'object', - }, - { - properties: { - ...customGroupNameJsonSchema, - ...customGroupSortJsonSchema, - ...singleCustomGroupJsonSchema, - }, - description: 'Custom group.', - additionalProperties: false, - type: 'object', - }, - ], - }, - description: 'Specifies custom groups.', - type: 'array', - }, partitionByComment: { ...partitionByCommentJsonSchema, description: 'Allows to use comments to separate the modules members into logical groups.', }, + customGroups: buildCustomGroupsArrayJsonSchema({ + singleCustomGroupJsonSchema, + }), partitionByNewLine: partitionByNewLineJsonSchema, specialCharacters: specialCharactersJsonSchema, newlinesBetween: newlinesBetweenJsonSchema, @@ -406,7 +371,7 @@ let analyzeModule = ({ getGroupNumber(options.groups, sortingNode) === options.groups.length, getGroupCompareOptions: groupNumber => - getCompareOptions(options, groupNumber), + getCustomGroupsCompareOptions(options, groupNumber), ignoreEslintDisabledNodes, }), ), diff --git a/rules/sort-modules.types.ts b/rules/sort-modules.types.ts index 055906f1..20b33709 100644 --- a/rules/sort-modules.types.ts +++ b/rules/sort-modules.types.ts @@ -1,5 +1,11 @@ import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema' +import { + buildCustomGroupModifiersJsonSchema, + buildCustomGroupSelectorJsonSchema, + elementNamePatternJsonSchema, +} from '../utils/common-json-schemas' + export type SortModulesOptions = [ Partial<{ type: 'alphabetical' | 'line-length' | 'natural' @@ -152,54 +158,16 @@ export let allModifiers: Modifier[] = [ 'export', ] -export let customGroupSortJsonSchema: Record = { - type: { - enum: ['alphabetical', 'line-length', 'natural', 'unsorted'], - description: 'Custom group sort type.', - type: 'string', - }, - order: { - description: 'Custom group sort order.', - enum: ['desc', 'asc'], - type: 'string', - }, -} - -export let customGroupNameJsonSchema: Record = { - groupName: { - description: 'Custom group name.', - type: 'string', - }, -} - /** * Ideally, we should generate as many schemas as there are selectors, and ensure * that users do not enter invalid modifiers for a given selector */ export let singleCustomGroupJsonSchema: Record = { - modifiers: { - items: { - enum: allModifiers, - type: 'string', - }, - description: 'Modifier filters.', - type: 'array', - }, - elementValuePattern: { - description: 'Element value pattern filter for properties.', - type: 'string', - }, decoratorNamePattern: { description: 'Decorator name pattern filter.', type: 'string', }, - selector: { - description: 'Selector filter.', - enum: allSelectors, - type: 'string', - }, - elementNamePattern: { - description: 'Element name pattern filter.', - type: 'string', - }, + modifiers: buildCustomGroupModifiersJsonSchema(allModifiers), + selector: buildCustomGroupSelectorJsonSchema(allSelectors), + elementNamePattern: elementNamePatternJsonSchema, } diff --git a/rules/sort-object-types-utils.ts b/rules/sort-object-types-utils.ts new file mode 100644 index 00000000..0dd7bd11 --- /dev/null +++ b/rules/sort-object-types-utils.ts @@ -0,0 +1,60 @@ +import type { + SingleCustomGroup, + AnyOfCustomGroup, + Modifier, + Selector, +} from './sort-object-types.types' + +import { matches } from '../utils/matches' + +interface CustomGroupMatchesProps { + customGroup: SingleCustomGroup | AnyOfCustomGroup + selectors: Selector[] + modifiers: Modifier[] + 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 (props.customGroup.modifiers) { + for (let modifier of props.customGroup.modifiers) { + if (!props.modifiers.includes(modifier)) { + 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-object-types.ts b/rules/sort-object-types.ts index 79d51585..451f9fa3 100644 --- a/rules/sort-object-types.ts +++ b/rules/sort-object-types.ts @@ -1,8 +1,12 @@ +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 { Modifier, Selector, Options } from './sort-object-types.types' import type { SortingNode } from '../typings' import { + buildCustomGroupsArrayJsonSchema, partitionByCommentJsonSchema, partitionByNewLineJsonSchema, specialCharactersJsonSchema, @@ -15,9 +19,13 @@ import { typeJsonSchema, } from '../utils/common-json-schemas' import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration' -import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' +import { validateGeneratedGroupsConfiguration } from './validate-generated-groups-configuration' +import { getCustomGroupsCompareOptions } from './get-custom-groups-compare-options' +import { generatePredefinedGroups } from '../utils/generate-predefined-groups' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' +import { singleCustomGroupJsonSchema } from './sort-object-types.types' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' +import { allModifiers, allSelectors } from './sort-object-types.types' import { hasPartitionComment } from '../utils/is-partition-comment' import { sortNodesByGroups } from '../utils/sort-nodes-by-groups' import { getCommentsBefore } from '../utils/get-comments-before' @@ -25,6 +33,7 @@ import { makeNewlinesFixes } from '../utils/make-newlines-fixes' import { getNewlinesErrors } from '../utils/get-newlines-errors' import { createEslintRule } from '../utils/create-eslint-rule' import { isMemberOptional } from '../utils/is-member-optional' +import { customGroupMatches } from './sort-object-types-utils' import { getLinesBetween } from '../utils/get-lines-between' import { getGroupNumber } from '../utils/get-group-number' import { getSourceCode } from '../utils/get-source-code' @@ -36,22 +45,12 @@ import { makeFixes } from '../utils/make-fixes' import { useGroups } from '../utils/use-groups' import { complete } from '../utils/complete' import { pairwise } from '../utils/pairwise' +import { matches } from '../utils/matches' -type Options = [ - Partial<{ - groupKind: 'required-first' | 'optional-first' | 'mixed' - customGroups: Record - type: 'alphabetical' | 'line-length' | 'natural' - partitionByComment: string[] | boolean | string - newlinesBetween: 'ignore' | 'always' | 'never' - specialCharacters: 'remove' | 'trim' | 'keep' - locales: NonNullable - groups: (Group[] | Group)[] - partitionByNewLine: boolean - order: 'desc' | 'asc' - ignoreCase: boolean - }>, -] +/** + * Cache computed groups by modifiers and selectors for performance + */ +let cachedGroupsByModifiersAndSelectors = new Map() type MESSAGE_ID = | 'missedSpacingBetweenObjectTypeMembers' @@ -63,15 +62,14 @@ interface SortObjectTypesSortingNode extends SortingNode { groupKind: 'required' | 'optional' } -type Group = 'multiline' | 'unknown' | T[number] | 'method' - -let defaultOptions: Required[0]> = { +let defaultOptions: Required = { partitionByComment: false, partitionByNewLine: false, newlinesBetween: 'ignore', specialCharacters: 'keep', type: 'alphabetical', groupKind: 'mixed', + ignorePattern: [], ignoreCase: true, customGroups: {}, locales: 'en-US', @@ -79,253 +77,373 @@ let defaultOptions: Required[0]> = { groups: [], } -export default createEslintRule, MESSAGE_ID>({ +export let jsonSchema: JSONSchema4 = { + properties: { + ignorePattern: { + description: + 'Specifies names or patterns for nodes that should be ignored by rule.', + items: { + type: 'string', + }, + type: 'array', + }, + partitionByComment: { + ...partitionByCommentJsonSchema, + description: + 'Allows you to use comments to separate members into logical groups.', + }, + customGroups: { + oneOf: [ + customGroupsJsonSchema, + buildCustomGroupsArrayJsonSchema({ singleCustomGroupJsonSchema }), + ], + }, + groupKind: { + enum: ['mixed', 'required-first', 'optional-first'], + description: 'Specifies top-level groups.', + type: 'string', + }, + partitionByNewLine: partitionByNewLineJsonSchema, + specialCharacters: specialCharactersJsonSchema, + newlinesBetween: newlinesBetweenJsonSchema, + ignoreCase: ignoreCaseJsonSchema, + locales: localesJsonSchema, + groups: groupsJsonSchema, + order: orderJsonSchema, + type: typeJsonSchema, + }, + additionalProperties: false, + type: 'object', +} + +export default createEslintRule({ + meta: { + messages: { + unexpectedObjectTypesGroupOrder: + 'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).', + missedSpacingBetweenObjectTypeMembers: + 'Missed spacing between "{{left}}" and "{{right}}" types.', + extraSpacingBetweenObjectTypeMembers: + 'Extra spacing between "{{left}}" and "{{right}}" types.', + unexpectedObjectTypesOrder: + 'Expected "{{right}}" to come before "{{left}}".', + }, + docs: { + url: 'https://perfectionist.dev/rules/sort-object-types', + description: 'Enforce sorted object types.', + recommended: true, + }, + schema: [jsonSchema], + type: 'suggestion', + fixable: 'code', + }, create: context => ({ - TSTypeLiteral: node => { - if (!isSortable(node.members)) { - return - } + TSTypeLiteral: node => + sortObjectTypeElements({ + availableMessageIds: { + missedSpacingBetweenMembers: 'missedSpacingBetweenObjectTypeMembers', + extraSpacingBetweenMembers: 'extraSpacingBetweenObjectTypeMembers', + unexpectedGroupOrder: 'unexpectedObjectTypesGroupOrder', + unexpectedOrder: 'unexpectedObjectTypesOrder', + }, + parentNodeName: + node.parent.type === 'TSTypeAliasDeclaration' + ? node.parent.id.name + : null, + elements: node.members, + context, + }), + }), + defaultOptions: [defaultOptions], + name: 'sort-object-types', +}) - let settings = getSettings(context.settings) - let options = complete(context.options.at(0), settings, defaultOptions) - validateGroupsConfiguration( - options.groups, - ['multiline', 'method', 'unknown'], - Object.keys(options.customGroups), - ) - validateNewlinesAndPartitionConfiguration(options) - - let sourceCode = getSourceCode(context) - let eslintDisabledLines = getEslintDisabledLines({ - ruleName: context.id, - sourceCode, - }) - - let formattedMembers: SortObjectTypesSortingNode[][] = - node.members.reduce( - (accumulator: SortObjectTypesSortingNode[][], member) => { - let name: string - let lastSortingNode = accumulator.at(-1)?.at(-1) - - let { setCustomGroups, defineGroup, getGroup } = useGroups(options) - - let formatName = (value: string): string => - value.replace(/[,;]$/u, '') - - if (member.type === 'TSPropertySignature') { - if (member.key.type === 'Identifier') { - ;({ name } = member.key) - } else if (member.key.type === 'Literal') { - name = `${member.key.value}` - } else { - name = sourceCode.text.slice( - member.range.at(0), - member.typeAnnotation?.range.at(0) ?? member.range.at(1), - ) - } - } else if (member.type === 'TSIndexSignature') { - let endIndex: number = - member.typeAnnotation?.range.at(0) ?? member.range.at(1)! - - name = formatName( - sourceCode.text.slice(member.range.at(0), endIndex), - ) - } else { - name = formatName( - sourceCode.text.slice(member.range.at(0), member.range.at(1)), - ) - } +export let sortObjectTypeElements = ({ + availableMessageIds, + parentNodeName, + elements, + context, +}: { + availableMessageIds: { + missedSpacingBetweenMembers: MessageIds + extraSpacingBetweenMembers: MessageIds + unexpectedGroupOrder: MessageIds + unexpectedOrder: MessageIds + } + context: RuleContext + elements: TSESTree.TypeElement[] + parentNodeName: string | null +}): void => { + if (!isSortable(elements)) { + return + } - setCustomGroups(options.customGroups, name) + let settings = getSettings(context.settings) + let options = complete(context.options.at(0), settings, defaultOptions) + validateGeneratedGroupsConfiguration({ + customGroups: options.customGroups, + selectors: allSelectors, + modifiers: allModifiers, + groups: options.groups, + }) + validateNewlinesAndPartitionConfiguration(options) - if ( - member.type === 'TSMethodSignature' || - (member.type === 'TSPropertySignature' && - member.typeAnnotation?.typeAnnotation.type === 'TSFunctionType') - ) { - defineGroup('method') - } + if ( + options.ignorePattern.some( + pattern => parentNodeName && matches(parentNodeName, pattern), + ) + ) { + return + } - if (member.loc.start.line !== member.loc.end.line) { - defineGroup('multiline') - } + let sourceCode = getSourceCode(context) + let eslintDisabledLines = getEslintDisabledLines({ + ruleName: context.id, + sourceCode, + }) - let sortingNode: SortObjectTypesSortingNode = { - isEslintDisabled: isNodeEslintDisabled( - member, - eslintDisabledLines, - ), - groupKind: isMemberOptional(member) ? 'optional' : 'required', - size: rangeToDiff(member, sourceCode), - addSafetySemicolonWhenInline: true, - group: getGroup(), - node: member, - name, - } + let formattedMembers: SortObjectTypesSortingNode[][] = elements.reduce( + (accumulator: SortObjectTypesSortingNode[][], typeElement) => { + if (typeElement.type === 'TSCallSignatureDeclaration') { + accumulator.push([]) + return accumulator + } - if ( - (options.partitionByComment && - hasPartitionComment( - options.partitionByComment, - getCommentsBefore({ - node: member, - sourceCode, - }), - )) || - (options.partitionByNewLine && - lastSortingNode && - getLinesBetween(sourceCode, lastSortingNode, sortingNode)) - ) { - accumulator.push([]) - } + let name: string + let lastSortingNode = accumulator.at(-1)?.at(-1) - accumulator.at(-1)?.push(sortingNode) + let { setCustomGroups, defineGroup, getGroup } = useGroups(options) - return accumulator - }, - [[]], + let formatName = (value: string): string => value.replace(/[,;]$/u, '') + + if (typeElement.type === 'TSPropertySignature') { + if (typeElement.key.type === 'Identifier') { + ;({ name } = typeElement.key) + } else if (typeElement.key.type === 'Literal') { + name = `${typeElement.key.value}` + } else { + let end: number = + typeElement.typeAnnotation?.range.at(0) ?? + typeElement.range.at(1)! - (typeElement.optional ? '?'.length : 0) + name = sourceCode.text.slice(typeElement.range.at(0), end) + } + } else if (typeElement.type === 'TSIndexSignature') { + let endIndex: number = + typeElement.typeAnnotation?.range.at(0) ?? typeElement.range.at(1)! + + name = formatName( + sourceCode.text.slice(typeElement.range.at(0), endIndex), ) - let groupKindOrder - if (options.groupKind === 'required-first') { - groupKindOrder = ['required', 'optional'] as const - } else if (options.groupKind === 'optional-first') { - groupKindOrder = ['optional', 'required'] as const + } else if ( + typeElement.type === 'TSMethodSignature' && + 'name' in typeElement.key + ) { + ;({ name } = typeElement.key) + /* v8 ignore next 8 - Unsure if we can reach it */ } else { - groupKindOrder = ['any'] as const - } - for (let nodes of formattedMembers) { - let filteredGroupKindNodes = groupKindOrder.map(groupKind => - nodes.filter( - currentNode => - groupKind === 'any' || currentNode.groupKind === groupKind, + name = formatName( + sourceCode.text.slice( + typeElement.range.at(0), + typeElement.range.at(1), ), ) - let sortNodesExcludingEslintDisabled = ( - ignoreEslintDisabledNodes: boolean, - ): SortObjectTypesSortingNode[] => - filteredGroupKindNodes.flatMap(groupedNodes => - sortNodesByGroups(groupedNodes, options, { - ignoreEslintDisabledNodes, - }), - ) - let sortedNodes = sortNodesExcludingEslintDisabled(false) - let sortedNodesExcludingEslintDisabled = - sortNodesExcludingEslintDisabled(true) + } + + let selectors: Selector[] = [] + let modifiers: Modifier[] = [] + + if (typeElement.type === 'TSIndexSignature') { + selectors.push('index-signature') + } + + if ( + typeElement.type === 'TSMethodSignature' || + (typeElement.type === 'TSPropertySignature' && + typeElement.typeAnnotation?.typeAnnotation.type === 'TSFunctionType') + ) { + selectors.push('method') + } - pairwise(nodes, (left, right) => { - let leftNumber = getGroupNumber(options.groups, left) - let rightNumber = getGroupNumber(options.groups, right) + if (typeElement.loc.start.line !== typeElement.loc.end.line) { + modifiers.push('multiline') + selectors.push('multiline') + } + + if ( + !selectors.includes('index-signature') && + !selectors.includes('method') + ) { + selectors.push('property') + } - let indexOfLeft = sortedNodes.indexOf(left) - let indexOfRight = sortedNodes.indexOf(right) - let indexOfRightExcludingEslintDisabled = - sortedNodesExcludingEslintDisabled.indexOf(right) + selectors.push('member') - let messageIds: MESSAGE_ID[] = [] + if (isMemberOptional(typeElement)) { + modifiers.push('optional') + } else { + modifiers.push('required') + } + + for (let predefinedGroup of generatePredefinedGroups({ + cache: cachedGroupsByModifiersAndSelectors, + selectors, + modifiers, + })) { + defineGroup(predefinedGroup) + } + if (Array.isArray(options.customGroups)) { + for (let customGroup of options.customGroups) { if ( - indexOfLeft > indexOfRight || - indexOfLeft >= indexOfRightExcludingEslintDisabled + customGroupMatches({ + elementName: name, + customGroup, + selectors, + modifiers, + }) ) { - messageIds.push( - leftNumber === rightNumber - ? 'unexpectedObjectTypesOrder' - : 'unexpectedObjectTypesGroupOrder', - ) + defineGroup(customGroup.groupName, true) + // If the custom group is not referenced in the `groups` option, it will be ignored + if (getGroup() === customGroup.groupName) { + break + } } + } + } else { + setCustomGroups(options.customGroups, name, { + override: true, + }) + } + + let sortingNode: SortObjectTypesSortingNode = { + isEslintDisabled: isNodeEslintDisabled( + typeElement, + eslintDisabledLines, + ), + groupKind: isMemberOptional(typeElement) ? 'optional' : 'required', + size: rangeToDiff(typeElement, sourceCode), + addSafetySemicolonWhenInline: true, + group: getGroup(), + node: typeElement, + name, + } - messageIds = [ - ...messageIds, - ...getNewlinesErrors({ - missedSpacingError: 'missedSpacingBetweenObjectTypeMembers', - extraSpacingError: 'extraSpacingBetweenObjectTypeMembers', - rightNum: rightNumber, - leftNum: leftNumber, + if ( + (options.partitionByComment && + hasPartitionComment( + options.partitionByComment, + getCommentsBefore({ + node: typeElement, sourceCode, - options, - right, - left, }), - ] - - for (let messageId of messageIds) { - context.report({ - fix: fixer => [ - ...makeFixes({ - sortedNodes: sortedNodesExcludingEslintDisabled, - sourceCode, - options, - fixer, - nodes, - }), - ...makeNewlinesFixes({ - sortedNodes: sortedNodesExcludingEslintDisabled, - sourceCode, - options, - fixer, - nodes, - }), - ], - data: { - right: toSingleLine(right.name), - left: toSingleLine(left.name), - rightGroup: right.group, - leftGroup: left.group, - }, - node: right.node, - messageId, - }) - } - }) + )) || + (options.partitionByNewLine && + lastSortingNode && + getLinesBetween(sourceCode, lastSortingNode, sortingNode)) + ) { + accumulator.push([]) } + + accumulator.at(-1)?.push(sortingNode) + + return accumulator }, - }), - meta: { - schema: [ - { - properties: { - partitionByComment: { - ...partitionByCommentJsonSchema, - description: - 'Allows you to use comments to separate the type members into logical groups.', - }, - groupKind: { - enum: ['mixed', 'required-first', 'optional-first'], - description: 'Specifies top-level groups.', - type: 'string', + [[]], + ) + + let groupKindOrder + if (options.groupKind === 'required-first') { + groupKindOrder = ['required', 'optional'] as const + } else if (options.groupKind === 'optional-first') { + groupKindOrder = ['optional', 'required'] as const + } else { + groupKindOrder = ['any'] as const + } + for (let nodes of formattedMembers) { + let filteredGroupKindNodes = groupKindOrder.map(groupKind => + nodes.filter( + currentNode => + groupKind === 'any' || currentNode.groupKind === groupKind, + ), + ) + let sortNodesExcludingEslintDisabled = ( + ignoreEslintDisabledNodes: boolean, + ): SortObjectTypesSortingNode[] => + filteredGroupKindNodes.flatMap(groupedNodes => + sortNodesByGroups(groupedNodes, options, { + getGroupCompareOptions: groupNumber => + getCustomGroupsCompareOptions(options, groupNumber), + ignoreEslintDisabledNodes, + }), + ) + let sortedNodes = sortNodesExcludingEslintDisabled(false) + let sortedNodesExcludingEslintDisabled = + sortNodesExcludingEslintDisabled(true) + + pairwise(nodes, (left, right) => { + let leftNumber = getGroupNumber(options.groups, left) + let rightNumber = getGroupNumber(options.groups, right) + + let indexOfLeft = sortedNodes.indexOf(left) + let indexOfRight = sortedNodes.indexOf(right) + let indexOfRightExcludingEslintDisabled = + sortedNodesExcludingEslintDisabled.indexOf(right) + + let messageIds: MessageIds[] = [] + + if ( + indexOfLeft > indexOfRight || + indexOfLeft >= indexOfRightExcludingEslintDisabled + ) { + messageIds.push( + leftNumber === rightNumber + ? availableMessageIds.unexpectedOrder + : availableMessageIds.unexpectedGroupOrder, + ) + } + + messageIds = [ + ...messageIds, + ...getNewlinesErrors({ + missedSpacingError: availableMessageIds.missedSpacingBetweenMembers, + extraSpacingError: availableMessageIds.extraSpacingBetweenMembers, + rightNum: rightNumber, + leftNum: leftNumber, + sourceCode, + options, + right, + left, + }), + ] + + for (let messageId of messageIds) { + context.report({ + fix: fixer => [ + ...makeFixes({ + sortedNodes: sortedNodesExcludingEslintDisabled, + sourceCode, + options, + fixer, + nodes, + }), + ...makeNewlinesFixes({ + sortedNodes: sortedNodesExcludingEslintDisabled, + sourceCode, + options, + fixer, + nodes, + }), + ], + data: { + right: toSingleLine(right.name), + left: toSingleLine(left.name), + rightGroup: right.group, + leftGroup: left.group, }, - partitionByNewLine: partitionByNewLineJsonSchema, - specialCharacters: specialCharactersJsonSchema, - newlinesBetween: newlinesBetweenJsonSchema, - customGroups: customGroupsJsonSchema, - ignoreCase: ignoreCaseJsonSchema, - locales: localesJsonSchema, - groups: groupsJsonSchema, - order: orderJsonSchema, - type: typeJsonSchema, - }, - additionalProperties: false, - type: 'object', - }, - ], - messages: { - unexpectedObjectTypesGroupOrder: - 'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).', - missedSpacingBetweenObjectTypeMembers: - 'Missed spacing between "{{left}}" and "{{right}}" types.', - extraSpacingBetweenObjectTypeMembers: - 'Extra spacing between "{{left}}" and "{{right}}" types.', - unexpectedObjectTypesOrder: - 'Expected "{{right}}" to come before "{{left}}".', - }, - docs: { - url: 'https://perfectionist.dev/rules/sort-object-types', - description: 'Enforce sorted object types.', - recommended: true, - }, - type: 'suggestion', - fixable: 'code', - }, - defaultOptions: [defaultOptions], - name: 'sort-object-types', -}) + node: right.node, + messageId, + }) + } + }) + } +} diff --git a/rules/sort-object-types.types.ts b/rules/sort-object-types.types.ts new file mode 100644 index 00000000..443af3b4 --- /dev/null +++ b/rules/sort-object-types.types.ts @@ -0,0 +1,156 @@ +import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema' + +import { + buildCustomGroupModifiersJsonSchema, + buildCustomGroupSelectorJsonSchema, + elementNamePatternJsonSchema, +} from '../utils/common-json-schemas' + +export type Options = [ + Partial<{ + customGroups: Record | CustomGroup[] + /** + * @deprecated for {@link `groups`} + */ + groupKind: 'required-first' | 'optional-first' | 'mixed' + type: 'alphabetical' | 'line-length' | 'natural' + partitionByComment: string[] | boolean | string + newlinesBetween: 'ignore' | 'always' | 'never' + specialCharacters: 'remove' | 'trim' | 'keep' + locales: NonNullable + groups: (Group[] | Group)[] + partitionByNewLine: boolean + ignorePattern: string[] + order: 'desc' | 'asc' + ignoreCase: boolean + }>, +] + +export type SingleCustomGroup = ( + | BaseSingleCustomGroup + | BaseSingleCustomGroup + | BaseSingleCustomGroup + | BaseSingleCustomGroup + | BaseSingleCustomGroup +) & + ElementNamePatternFilterCustomGroup + +export type CustomGroup = ( + | { + order?: Options[0]['order'] + type?: Options[0]['type'] + } + | { + type?: 'unsorted' + } +) & + (SingleCustomGroup | AnyOfCustomGroup) & { + groupName: string + } + +export type Selector = + | IndexSignatureSelector + | MultilineSelector + | PropertySelector + | MemberSelector + | MethodSelector + +export type Modifier = MultilineModifier | RequiredModifier | OptionalModifier + +export interface AnyOfCustomGroup { + anyOf: SingleCustomGroup[] +} + +/** + * Only used in code as well + */ +interface AllowedModifiersPerSelector { + property: MultilineModifier | OptionalModifier | RequiredModifier + member: MultilineModifier | OptionalModifier | RequiredModifier + method: MultilineModifier | OptionalModifier | RequiredModifier + multiline: OptionalModifier | RequiredModifier + 'index-signature': never +} + +type IndexSignatureGroup = + `${OptionalModifierPrefix | RequiredModifierPrefix}${MultilineModifierPrefix}${IndexSignatureSelector}` + +/** + * Only used in code, so I don't know if it's worth maintaining this. + */ +type Group = + | IndexSignatureGroup + | MultilineGroup + | PropertyGroup + | MethodGroup + | MemberGroup + | 'unknown' + | string + +type PropertyGroup = + `${OptionalModifierPrefix | RequiredModifierPrefix}${MultilineModifierPrefix}${PropertySelector}` + +interface BaseSingleCustomGroup { + modifiers?: AllowedModifiersPerSelector[T][] + selector?: T +} + +type MemberGroup = + `${OptionalModifierPrefix | RequiredModifierPrefix}${MultilineModifierPrefix}${MemberSelector}` + +type MethodGroup = + `${OptionalModifierPrefix | RequiredModifierPrefix}${MultilineModifierPrefix}${MethodSelector}` + +type MultilineGroup = + `${OptionalModifierPrefix | RequiredModifierPrefix}${MultilineSelector}` + +interface ElementNamePatternFilterCustomGroup { + elementNamePattern?: string +} + +type MultilineModifierPrefix = WithDashSuffixOrEmpty + +type RequiredModifierPrefix = WithDashSuffixOrEmpty + +type OptionalModifierPrefix = WithDashSuffixOrEmpty + +type WithDashSuffixOrEmpty = `${T}-` | '' + +type IndexSignatureSelector = 'index-signature' + +/** + * @deprecated For {@link `MultilineModifier`} + */ +type MultilineSelector = 'multiline' + +type MultilineModifier = 'multiline' + +type RequiredModifier = 'required' + +type OptionalModifier = 'optional' + +type PropertySelector = 'property' + +type MemberSelector = 'member' + +type MethodSelector = 'method' + +export let allSelectors: Selector[] = [ + 'index-signature', + 'member', + 'method', + 'multiline', + 'property', +] + +export let allModifiers: Modifier[] = ['optional', 'required', 'multiline'] + +/** + * Ideally, we should generate as many schemas as there are selectors, and ensure + * that users do not enter invalid modifiers for a given selector + */ +export let singleCustomGroupJsonSchema: Record = { + modifiers: buildCustomGroupModifiersJsonSchema(allModifiers), + selector: buildCustomGroupSelectorJsonSchema(allSelectors), + elementNamePattern: elementNamePatternJsonSchema, +} diff --git a/rules/validate-generated-groups-configuration.ts b/rules/validate-generated-groups-configuration.ts index 21b8d0e4..eba894d9 100644 --- a/rules/validate-generated-groups-configuration.ts +++ b/rules/validate-generated-groups-configuration.ts @@ -3,7 +3,7 @@ import type { Modifier, Selector } from './sort-classes.types' import { validateNoDuplicatedGroups } from '../utils/validate-groups-configuration' interface Props { - customGroups: BaseCustomGroup[] + customGroups: Record | BaseCustomGroup[] groups: (string[] | string)[] selectors: string[] modifiers: string[] @@ -20,7 +20,9 @@ export let validateGeneratedGroupsConfiguration = ({ groups, }: Props): void => { let availableCustomGroupNames = new Set( - customGroups.map(customGroup => customGroup.groupName), + Array.isArray(customGroups) + ? customGroups.map(customGroup => customGroup.groupName) + : Object.keys(customGroups), ) let invalidGroups = groups .flat() diff --git a/test/sort-interfaces.test.ts b/test/sort-interfaces.test.ts index 567f6c44..d2d21de4 100644 --- a/test/sort-interfaces.test.ts +++ b/test/sort-interfaces.test.ts @@ -21,7 +21,7 @@ describe(ruleName, () => { describe(`${ruleName}: sorting by alphabetical order`, () => { let type = 'alphabetical-order' - let options: Options[0] = { + let options: Options[0] = { type: 'alphabetical', ignoreCase: true, order: 'asc', @@ -235,15 +235,15 @@ describe(ruleName, () => { errors: [ { data: { - left: 'c()', right: 'a', + left: 'c', }, messageId: 'unexpectedInterfacePropertiesOrder', }, { data: { - left: 'e()', right: 'd', + left: 'e', }, messageId: 'unexpectedInterfacePropertiesOrder', }, @@ -499,6 +499,390 @@ describe(ruleName, () => { }, ) + ruleTester.run( + `${ruleName}(${type}): sorts complex predefined groups`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + leftGroup: 'required-property', + rightGroup: 'index-signature', + right: '[key: string]', + left: 'a', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + { + data: { + rightGroup: 'optional-multiline', + leftGroup: 'index-signature', + left: '[key: string]', + right: 'b', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + { + data: { + leftGroup: 'optional-multiline', + rightGroup: 'required-method', + right: 'c', + left: 'b', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + options: [ + { + ...options, + groups: [ + 'unknown', + 'required-method', + 'optional-multiline', + 'index-signature', + 'required-property', + ], + }, + ], + output: dedent` + interface Interface { + c(): void + b?: { + property: string; + } + [key: string]: string; + a: string + } + `, + code: dedent` + interface Interface { + a: string + [key: string]: string; + b?: { + property: string; + } + c(): void + } + `, + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize selectors over modifiers quantity`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + leftGroup: 'required-property', + rightGroup: 'method', + left: 'property', + right: 'method', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + options: [ + { + ...options, + groups: ['method', 'required-property'], + }, + ], + output: dedent` + interface Interface { + method(): void + property: string + } + `, + code: dedent` + interface Interface { + property: string + method(): void + } + `, + }, + ], + valid: [], + }, + ) + + describe(`${ruleName}(${type}): selectors priority`, () => { + ruleTester.run( + `${ruleName}(${type}): prioritize index-signature over multiline`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'index-signature', + left: 'multilineProperty', + right: '[key: string]', + leftGroup: 'multiline', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + output: dedent` + interface Interface { + [key: string]: string; + multilineProperty: { + a: string + } + } + `, + code: dedent` + interface Interface { + multilineProperty: { + a: string + } + [key: string]: string; + } + `, + options: [ + { + ...options, + groups: ['index-signature', 'multiline'], + }, + ], + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize method over multiline`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + left: 'multilineProperty', + leftGroup: 'multiline', + rightGroup: 'method', + right: 'method', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + output: dedent` + interface Interface { + method(): string + multilineProperty: { + a: string + } + } + `, + code: dedent` + interface Interface { + multilineProperty: { + a: string + } + method(): string + } + `, + options: [ + { + ...options, + groups: ['method', 'multiline'], + }, + ], + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize multiline over property`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + right: 'multilineProperty', + rightGroup: 'multiline', + leftGroup: 'property', + left: 'property', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + output: dedent` + interface Interface { + multilineProperty: { + a: string + } + property: string + } + `, + code: dedent` + interface Interface { + property: string + multilineProperty: { + a: string + } + } + `, + options: [ + { + ...options, + groups: ['multiline', 'property'], + }, + ], + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize property over member`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'property', + leftGroup: 'member', + right: 'property', + left: 'method', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + output: dedent` + interface Interface { + property: string + method(): string + } + `, + code: dedent` + interface Interface { + method(): string + property: string + } + `, + options: [ + { + ...options, + groups: ['property', 'member'], + }, + ], + }, + ], + valid: [], + }, + ) + }) + + describe(`${ruleName}(${type}): modifiers priority`, () => { + ruleTester.run( + `${ruleName}(${type}): prioritize multiline over optional`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'multiline-property', + leftGroup: 'optional-property', + right: 'multilineProperty', + left: 'optionalProperty', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + output: dedent` + interface Interface { + multilineProperty: { + a: string + } + optionalProperty?: string + } + `, + code: dedent` + interface Interface { + optionalProperty?: string + multilineProperty: { + a: string + } + } + `, + options: [ + { + ...options, + groups: ['multiline-property', 'optional-property'], + }, + ], + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize multiline over required`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'multiline-property', + leftGroup: 'required-property', + right: 'multilineProperty', + left: 'requiredProperty', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + output: dedent` + interface Interface { + multilineProperty: { + a: string + } + requiredProperty: string + } + `, + code: dedent` + interface Interface { + requiredProperty: string + multilineProperty: { + a: string + } + } + `, + options: [ + { + ...options, + groups: ['multiline-property', 'required-property'], + }, + ], + }, + ], + valid: [], + }, + ) + }) + ruleTester.run( `${ruleName}(${type}): allows to set groups for sorting`, rule, @@ -588,6 +972,352 @@ describe(ruleName, () => { }, ) + describe(`${ruleName}: custom groups`, () => { + ruleTester.run(`${ruleName}: filters on selector and modifiers`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'unusedCustomGroup', + modifiers: ['optional'], + selector: 'method', + }, + { + groupName: 'optionalPropertyGroup', + modifiers: ['optional'], + selector: 'property', + }, + { + groupName: 'propertyGroup', + selector: 'property', + }, + ], + groups: ['propertyGroup', 'optionalPropertyGroup'], + }, + ], + errors: [ + { + data: { + leftGroup: 'optionalPropertyGroup', + rightGroup: 'propertyGroup', + right: 'c', + left: 'b', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + output: dedent` + interface Interface { + c: string + a?: string + b?: string + } + `, + code: dedent` + interface Interface { + a?: string + b?: string + c: string + } + `, + }, + ], + valid: [], + }) + + ruleTester.run(`${ruleName}: filters on elementNamePattern`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'propertiesStartingWithHello', + elementNamePattern: 'hello*', + selector: 'property', + }, + ], + groups: ['propertiesStartingWithHello', 'unknown'], + }, + ], + errors: [ + { + data: { + rightGroup: 'propertiesStartingWithHello', + right: 'helloProperty', + leftGroup: 'unknown', + left: 'method', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + output: dedent` + interface Interface { + helloProperty: string + a: string + b: string + method(): void + } + `, + code: dedent` + interface Interface { + a: string + b: string + method(): void + helloProperty: string + } + `, + }, + ], + valid: [], + }) + + ruleTester.run( + `${ruleName}: sort custom groups by overriding 'type' and 'order'`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + right: 'bb', + left: 'a', + }, + messageId: 'unexpectedInterfacePropertiesOrder', + }, + { + data: { + right: 'ccc', + left: 'bb', + }, + messageId: 'unexpectedInterfacePropertiesOrder', + }, + { + data: { + right: 'dddd', + left: 'ccc', + }, + messageId: 'unexpectedInterfacePropertiesOrder', + }, + { + data: { + rightGroup: 'reversedPropertiesByLineLength', + leftGroup: 'unknown', + left: 'method', + right: 'eee', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + options: [ + { + customGroups: [ + { + groupName: 'reversedPropertiesByLineLength', + selector: 'property', + type: 'line-length', + order: 'desc', + }, + ], + groups: ['reversedPropertiesByLineLength', 'unknown'], + type: 'alphabetical', + order: 'asc', + }, + ], + output: dedent` + interface Interface { + dddd: string + ccc: string + eee: string + bb: string + ff: string + a: string + g: string + anotherMethod(): void + method(): void + yetAnotherMethod(): void + } + `, + code: dedent` + interface Interface { + a: string + bb: string + ccc: string + dddd: string + method(): void + eee: string + ff: string + g: string + anotherMethod(): void + yetAnotherMethod(): void + } + `, + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}: does not sort custom groups with 'unsorted' type`, + rule, + { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'unsortedProperties', + selector: 'property', + type: 'unsorted', + }, + ], + groups: ['unsortedProperties', 'unknown'], + }, + ], + errors: [ + { + data: { + rightGroup: 'unsortedProperties', + leftGroup: 'unknown', + left: 'method', + right: 'c', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + ], + output: dedent` + interface Interface { + b + a + d + e + c + method(): void + } + `, + code: dedent` + interface Interface { + b + a + d + e + method(): void + c + } + `, + }, + ], + valid: [], + }, + ) + + ruleTester.run(`${ruleName}: sort custom group blocks`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + anyOf: [ + { + modifiers: ['required'], + selector: 'property', + }, + { + modifiers: ['optional'], + selector: 'method', + }, + ], + groupName: 'requiredPropertiesAndOptionalMethods', + }, + ], + groups: [ + ['requiredPropertiesAndOptionalMethods', 'index-signature'], + 'unknown', + ], + }, + ], + errors: [ + { + data: { + rightGroup: 'requiredPropertiesAndOptionalMethods', + leftGroup: 'unknown', + right: 'd', + left: 'c', + }, + messageId: 'unexpectedInterfacePropertiesGroupOrder', + }, + { + data: { + right: '[key: string]', + left: 'e', + }, + messageId: 'unexpectedInterfacePropertiesOrder', + }, + ], + output: dedent` + interface Interface { + [key: string]: string + a: string + d?: () => void + e: string + b(): void + c?: string + } + `, + code: dedent` + interface Interface { + a: string + b(): void + c?: string + d?: () => void + e: string + [key: string]: string + } + `, + }, + ], + 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` + interface Interface { + iHaveFooInMyName: string + meTooIHaveFoo: string + a: string + b: string + } + `, + }, + ], + invalid: [], + }, + ) + }) + ruleTester.run( `${ruleName}(${type}): allows to use regex for custom groups`, rule, @@ -1490,15 +2220,15 @@ describe(ruleName, () => { errors: [ { data: { - left: 'c()', right: 'a', + left: 'c', }, messageId: 'unexpectedInterfacePropertiesOrder', }, { data: { - left: 'e()', right: 'd', + left: 'e', }, messageId: 'unexpectedInterfacePropertiesOrder', }, @@ -2217,7 +2947,7 @@ describe(ruleName, () => { errors: [ { data: { - right: 'c()', + right: 'c', left: 'a', }, messageId: 'unexpectedInterfacePropertiesOrder', diff --git a/test/sort-object-types.test.ts b/test/sort-object-types.test.ts index b3f3a1c9..d0a815f6 100644 --- a/test/sort-object-types.test.ts +++ b/test/sort-object-types.test.ts @@ -210,8 +210,8 @@ describe(ruleName, () => { errors: [ { data: { - left: 'func(): void', right: 'arrowFunc', + left: 'func', }, messageId: 'unexpectedObjectTypesOrder', }, @@ -268,6 +268,390 @@ describe(ruleName, () => { ], }) + ruleTester.run( + `${ruleName}(${type}): sorts complex predefined groups`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + leftGroup: 'required-property', + rightGroup: 'index-signature', + right: '[key: string]', + left: 'a', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + { + data: { + rightGroup: 'optional-multiline', + leftGroup: 'index-signature', + left: '[key: string]', + right: 'b', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + { + data: { + leftGroup: 'optional-multiline', + rightGroup: 'required-method', + right: 'c', + left: 'b', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + options: [ + { + ...options, + groups: [ + 'unknown', + 'required-method', + 'optional-multiline', + 'index-signature', + 'required-property', + ], + }, + ], + output: dedent` + type Type = { + c(): void + b?: { + property: string; + } + [key: string]: string; + a: string + } + `, + code: dedent` + type Type = { + a: string + [key: string]: string; + b?: { + property: string; + } + c(): void + } + `, + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize selectors over modifiers quantity`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + leftGroup: 'required-property', + rightGroup: 'method', + left: 'property', + right: 'method', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + options: [ + { + ...options, + groups: ['method', 'required-property'], + }, + ], + output: dedent` + type Type = { + method(): void + property: string + } + `, + code: dedent` + type Type = { + property: string + method(): void + } + `, + }, + ], + valid: [], + }, + ) + + describe(`${ruleName}(${type}): selectors priority`, () => { + ruleTester.run( + `${ruleName}(${type}): prioritize index-signature over multiline`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'index-signature', + left: 'multilineProperty', + right: '[key: string]', + leftGroup: 'multiline', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + output: dedent` + type Type = { + [key: string]: string; + multilineProperty: { + a: string + } + } + `, + code: dedent` + type Type = { + multilineProperty: { + a: string + } + [key: string]: string; + } + `, + options: [ + { + ...options, + groups: ['index-signature', 'multiline'], + }, + ], + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize method over multiline`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + left: 'multilineProperty', + leftGroup: 'multiline', + rightGroup: 'method', + right: 'method', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + output: dedent` + type Type = { + method(): string + multilineProperty: { + a: string + } + } + `, + code: dedent` + type Type = { + multilineProperty: { + a: string + } + method(): string + } + `, + options: [ + { + ...options, + groups: ['method', 'multiline'], + }, + ], + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize multiline over property`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + right: 'multilineProperty', + rightGroup: 'multiline', + leftGroup: 'property', + left: 'property', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + output: dedent` + type Type = { + multilineProperty: { + a: string + } + property: string + } + `, + code: dedent` + type Type = { + property: string + multilineProperty: { + a: string + } + } + `, + options: [ + { + ...options, + groups: ['multiline', 'property'], + }, + ], + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize property over member`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'property', + leftGroup: 'member', + right: 'property', + left: 'method', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + output: dedent` + type Type = { + property: string + method(): string + } + `, + code: dedent` + type Type = { + method(): string + property: string + } + `, + options: [ + { + ...options, + groups: ['property', 'member'], + }, + ], + }, + ], + valid: [], + }, + ) + }) + + describe(`${ruleName}(${type}): modifiers priority`, () => { + ruleTester.run( + `${ruleName}(${type}): prioritize multiline over optional`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'multiline-property', + leftGroup: 'optional-property', + right: 'multilineProperty', + left: 'optionalProperty', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + output: dedent` + type Type = { + multilineProperty: { + a: string + } + optionalProperty?: string + } + `, + code: dedent` + type Type = { + optionalProperty?: string + multilineProperty: { + a: string + } + } + `, + options: [ + { + ...options, + groups: ['multiline-property', 'optional-property'], + }, + ], + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}(${type}): prioritize multiline over required`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + rightGroup: 'multiline-property', + leftGroup: 'required-property', + right: 'multilineProperty', + left: 'requiredProperty', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + output: dedent` + type Type = { + multilineProperty: { + a: string + } + requiredProperty: string + } + `, + code: dedent` + type Type = { + requiredProperty: string + multilineProperty: { + a: string + } + } + `, + options: [ + { + ...options, + groups: ['multiline-property', 'required-property'], + }, + ], + }, + ], + valid: [], + }, + ) + }) + ruleTester.run( `${ruleName}(${type}): allows to set groups for sorting`, rule, @@ -277,52 +661,54 @@ describe(ruleName, () => { errors: [ { data: { + rightGroup: 'multiline', leftGroup: 'unknown', - rightGroup: 'b', - right: 'b', - left: 'a', + right: 'd', + left: 'c', }, messageId: 'unexpectedObjectTypesGroupOrder', }, { data: { - right: 'e', - left: 'f', + leftGroup: 'multiline', + rightGroup: 'g', + right: 'g', + left: 'd', }, - messageId: 'unexpectedObjectTypesOrder', + messageId: 'unexpectedObjectTypesGroupOrder', }, ], - output: [ - dedent` - type Type = { - b: 'bb' - a: 'aaa' - c: 'c' - d: { - e: 'ee' - f: 'f' - } + output: dedent` + type Type = { + g: 'g' + d: { + e: 'e' + f: 'f' } - `, - ], + a: 'aaa' + b: 'bb' + c: 'c' + } + `, code: dedent` type Type = { a: 'aaa' b: 'bb' c: 'c' d: { + e: 'e' f: 'f' - e: 'ee' } + g: 'g' } `, options: [ { ...options, customGroups: { - b: 'b', + g: 'g', }, - groups: ['b', 'unknown', 'multiline'], + groups: ['g', 'multiline', 'unknown'], }, ], }, @@ -331,22 +717,23 @@ describe(ruleName, () => { { code: dedent` type Type = { - b: 'bb' - a: 'aaa' - c: 'c' + g: 'g' d: { - e: 'ee' + e: 'e' f: 'f' } + a: 'aaa' + b: 'bb' + c: 'c' } `, options: [ { ...options, customGroups: { - b: 'b', + g: 'g', }, - groups: ['b', 'unknown', 'multiline'], + groups: ['g', 'multiline', 'unknown'], }, ], }, @@ -354,6 +741,352 @@ describe(ruleName, () => { }, ) + describe(`${ruleName}: custom groups`, () => { + ruleTester.run(`${ruleName}: filters on selector and modifiers`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'unusedCustomGroup', + modifiers: ['optional'], + selector: 'method', + }, + { + groupName: 'optionalPropertyGroup', + modifiers: ['optional'], + selector: 'property', + }, + { + groupName: 'propertyGroup', + selector: 'property', + }, + ], + groups: ['propertyGroup', 'optionalPropertyGroup'], + }, + ], + errors: [ + { + data: { + leftGroup: 'optionalPropertyGroup', + rightGroup: 'propertyGroup', + right: 'c', + left: 'b', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + output: dedent` + type Type = { + c: string + a?: string + b?: string + } + `, + code: dedent` + type Type = { + a?: string + b?: string + c: string + } + `, + }, + ], + valid: [], + }) + + ruleTester.run(`${ruleName}: filters on elementNamePattern`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'propertiesStartingWithHello', + elementNamePattern: 'hello*', + selector: 'property', + }, + ], + groups: ['propertiesStartingWithHello', 'unknown'], + }, + ], + errors: [ + { + data: { + rightGroup: 'propertiesStartingWithHello', + right: 'helloProperty', + leftGroup: 'unknown', + left: 'method', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + output: dedent` + type Type = { + helloProperty: string + a: string + b: string + method(): void + } + `, + code: dedent` + type Type = { + a: string + b: string + method(): void + helloProperty: string + } + `, + }, + ], + valid: [], + }) + + ruleTester.run( + `${ruleName}: sort custom groups by overriding 'type' and 'order'`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + right: 'bb', + left: 'a', + }, + messageId: 'unexpectedObjectTypesOrder', + }, + { + data: { + right: 'ccc', + left: 'bb', + }, + messageId: 'unexpectedObjectTypesOrder', + }, + { + data: { + right: 'dddd', + left: 'ccc', + }, + messageId: 'unexpectedObjectTypesOrder', + }, + { + data: { + rightGroup: 'reversedPropertiesByLineLength', + leftGroup: 'unknown', + left: 'method', + right: 'eee', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + options: [ + { + customGroups: [ + { + groupName: 'reversedPropertiesByLineLength', + selector: 'property', + type: 'line-length', + order: 'desc', + }, + ], + groups: ['reversedPropertiesByLineLength', 'unknown'], + type: 'alphabetical', + order: 'asc', + }, + ], + output: dedent` + type Type = { + dddd: string + ccc: string + eee: string + bb: string + ff: string + a: string + g: string + anotherMethod(): void + method(): void + yetAnotherMethod(): void + } + `, + code: dedent` + type Type = { + a: string + bb: string + ccc: string + dddd: string + method(): void + eee: string + ff: string + g: string + anotherMethod(): void + yetAnotherMethod(): void + } + `, + }, + ], + valid: [], + }, + ) + + ruleTester.run( + `${ruleName}: does not sort custom groups with 'unsorted' type`, + rule, + { + invalid: [ + { + options: [ + { + customGroups: [ + { + groupName: 'unsortedProperties', + selector: 'property', + type: 'unsorted', + }, + ], + groups: ['unsortedProperties', 'unknown'], + }, + ], + errors: [ + { + data: { + rightGroup: 'unsortedProperties', + leftGroup: 'unknown', + left: 'method', + right: 'c', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + ], + output: dedent` + type Type = { + b + a + d + e + c + method(): void + } + `, + code: dedent` + type Type = { + b + a + d + e + method(): void + c + } + `, + }, + ], + valid: [], + }, + ) + + ruleTester.run(`${ruleName}: sort custom group blocks`, rule, { + invalid: [ + { + options: [ + { + customGroups: [ + { + anyOf: [ + { + modifiers: ['required'], + selector: 'property', + }, + { + modifiers: ['optional'], + selector: 'method', + }, + ], + groupName: 'requiredPropertiesAndOptionalMethods', + }, + ], + groups: [ + ['requiredPropertiesAndOptionalMethods', 'index-signature'], + 'unknown', + ], + }, + ], + errors: [ + { + data: { + rightGroup: 'requiredPropertiesAndOptionalMethods', + leftGroup: 'unknown', + right: 'd', + left: 'c', + }, + messageId: 'unexpectedObjectTypesGroupOrder', + }, + { + data: { + right: '[key: string]', + left: 'e', + }, + messageId: 'unexpectedObjectTypesOrder', + }, + ], + output: dedent` + type Type = { + [key: string]: string + a: string + d?: () => void + e: string + b(): void + c?: string + } + `, + code: dedent` + type Type = { + a: string + b(): void + c?: string + d?: () => void + e: string + [key: string]: string + } + `, + }, + ], + 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` + type Type = { + iHaveFooInMyName: string + meTooIHaveFoo: string + a: string + b: string + } + `, + }, + ], + invalid: [], + }, + ) + }) + ruleTester.run( `${ruleName}(${type}): allows to use regex for custom groups`, rule, @@ -1276,15 +2009,15 @@ describe(ruleName, () => { errors: [ { data: { - left: '[name in v]?', + left: '[name in v]', right: '8', }, messageId: 'unexpectedObjectTypesOrder', }, { data: { - left: 'func(): void', right: 'arrowFunc', + left: 'func', }, messageId: 'unexpectedObjectTypesOrder', }, @@ -1859,15 +2592,15 @@ describe(ruleName, () => { }, { data: { - right: 'func(): void', + right: 'func', left: '8', }, messageId: 'unexpectedObjectTypesOrder', }, { data: { - left: 'func(): void', right: 'arrowFunc', + left: 'func', }, messageId: 'unexpectedObjectTypesOrder', }, @@ -2371,6 +3104,25 @@ describe(ruleName, () => { }, ) + ruleTester.run(`${ruleName}: allows to ignore object types`, rule, { + valid: [ + { + code: dedent` + type IgnoreType = { + b: 'b' + a: 'a' + } + `, + options: [ + { + ignorePattern: ['Ignore'], + }, + ], + }, + ], + invalid: [], + }) + let eslintDisableRuleTesterName = `${ruleName}: supports 'eslint-disable' for individual nodes` ruleTester.run(eslintDisableRuleTesterName, rule, { invalid: [ diff --git a/utils/common-json-schemas.ts b/utils/common-json-schemas.ts index 4e051026..efdd0783 100644 --- a/utils/common-json-schemas.ts +++ b/utils/common-json-schemas.ts @@ -105,3 +105,90 @@ export let newlinesBetweenJsonSchema: JSONSchema4 = { enum: ['ignore', 'always', 'never'], type: 'string', } + +let customGroupSortJsonSchema: Record = { + type: { + enum: ['alphabetical', 'line-length', 'natural', 'unsorted'], + description: 'Custom group sort type.', + type: 'string', + }, + order: { + description: 'Custom group sort order.', + enum: ['desc', 'asc'], + type: 'string', + }, +} + +let customGroupNameJsonSchema: Record = { + groupName: { + description: 'Custom group name.', + type: 'string', + }, +} + +export let buildCustomGroupsArrayJsonSchema = ({ + singleCustomGroupJsonSchema, +}: { + singleCustomGroupJsonSchema?: Record +}): JSONSchema4 => ({ + items: { + oneOf: [ + { + properties: { + ...customGroupNameJsonSchema, + ...customGroupSortJsonSchema, + anyOf: { + items: { + properties: { + ...singleCustomGroupJsonSchema, + }, + description: 'Custom group.', + additionalProperties: false, + type: 'object', + }, + type: 'array', + }, + }, + description: 'Custom group block.', + additionalProperties: false, + type: 'object', + }, + { + properties: { + ...customGroupNameJsonSchema, + ...customGroupSortJsonSchema, + ...singleCustomGroupJsonSchema, + }, + description: 'Custom group.', + additionalProperties: false, + type: 'object', + }, + ], + }, + description: 'Specifies custom groups.', + type: 'array', +}) + +export let buildCustomGroupModifiersJsonSchema = ( + modifiers: string[], +): JSONSchema4 => ({ + items: { + enum: modifiers, + type: 'string', + }, + description: 'Modifier filters.', + type: 'array', +}) + +export let buildCustomGroupSelectorJsonSchema = ( + selectors: string[], +): JSONSchema4 => ({ + description: 'Selector filter.', + enum: selectors, + type: 'string', +}) + +export let elementNamePatternJsonSchema: JSONSchema4 = { + description: 'Element name pattern filter.', + type: 'string', +}