diff --git a/docs/content/configs/recommended-custom.mdx b/docs/content/configs/recommended-custom.mdx new file mode 100644 index 00000000..0214801c --- /dev/null +++ b/docs/content/configs/recommended-custom.mdx @@ -0,0 +1,199 @@ +--- +title: recommended-custom +description: Learn more about the recommended custom ESLint Plugin Perfectionist configuration for sorting and organizing your code. Take customization to new levels while maintaining a consistent coding style with this setup +shortDescription: All plugin rules with your own custom sorting +keywords: + - eslint + - recommended custom config + - eslint configuration + - coding standards + - code quality + - javascript linting + - custom sorting + - eslint-plugin-perfectionist +--- + +import CodeTabs from '../../components/CodeTabs.svelte' +import { dedent } from 'ts-dedent' + +Configuration for the `eslint-plugin-perfectionist` plugin, which provides all plugin rules with your predefined custom ordered alphabet. + +This configuration allows you to define your own custom order for sorting elements in your codebase as you truly desire. + +## When to Use + +Each rule in `eslint-plugin-perfectionist` offers a lot of options that should suit most use cases. + +If this is not enough, you may define your own alphabet and use the `recommended-custom` configuration to enforce a consistent custom order across various data structures in your codebase. + +Use this configuration to precisely tune how elements should be sorted while keeping readability and maintainability to their highest levels. + +## Usage + +You must provide an `alphabet` option in the `perfectionist` settings object or for each rule individually. +This option should be a string that represents an ordered alphabet. + +Example: `01234564789abcdef...` + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + + naturalCompare(a, b)) + .getCharacters(); + + export default [ + { + ...perfectionist.configs['recommended-custom'], + settings: { + perfectionist: { + alphabet: myCustomAlphabet + } + } + } + ] + `, + name: 'Flat Config', + value: 'flat', + }, + { + source: dedent` + // .eslintrc.js + import { Alphabet } from 'eslint-plugin-perfectionist/alphabet' + import perfectionist from 'eslint-plugin-perfectionist' + import naturalCompare from 'natural-compare-lite'; + + const myCustomAlphabet = Alphabet + .generateRecommendedAlphabet() + .sortingBy((a, b) => naturalCompare(a, b)) + .getCharacters(); + + module.exports = { + extends: [ + 'plugin:perfectionist/recommended-custom-legacy', + ], + settings: { + perfectionist: { + alphabet: myCustomAlphabet + } + } + } + `, + name: 'Legacy Config', + value: 'legacy', + }, +]} +type="config-type" +client:load +lang="tsx" +/> + +## Alphabet class + +The `Alphabet` class from `eslint-plugin-perfectionist/alphabet` provides a set of methods to generate and manipulate alphabets. + +### Static generators + +#### - `static generateCompleteAlphabet(): Alphabet` + +Generates an alphabet containing all characters from the Unicode standard except for irrelevant [Unicode planes](https://en.wikipedia.org/wiki/Plane_(Unicode)). +Contains the Unicode planes 0, 1, 2 and 3. + +#### - `static generateRecommendedAlphabet(): Alphabet` + +Generates an alphabet containing relevant characters from the Unicode standard. Contains the [Unicode planes](https://en.wikipedia.org/wiki/Plane_(Unicode)) 0 and 1. + +#### - `static generateFrom(values: string[] | string): Alphabet` + +Generates an alphabet from the given characters. + +### Adding/Removing characters + +#### - `pushCharacters(values: string[] | string): this` + +Adds specific characters to the end of the alphabet. + +#### - `removeCharacters(values: string[] | string): this` + +Removes specific characters from the alphabet. + +#### - `removeUnicodeRange({ start: number; end: number }): this` + +Removes specific characters from the alphabet by their range + +### Sorters + +#### - `sortByLocaleCompare(locales?: Intl.LocalesArgument): this` + +Sorts the alphabet by the locale order of the characters. + +#### - `sortByNaturalSort(locale?: string): this` + +Sorts the alphabet by the natural order of the characters using [natural-orderby](https://github.com/yobacca/natural-orderby). + +#### - `sortByCharCodeAt(): this` + +Sorts the alphabet by the character code point. + +#### - `sortBy(sortingFunction: (characterA: string, characterB: string) => number): this` + +Sorts the alphabet by the sorting function provided + +#### - `reverse(): this` + +Reverses the alphabet. + +### Other methods + +#### - `prioritizeCase(casePriority: 'lowercase' | 'uppercase'): this` + +For each character with a lower and upper case, permutes the two cases so that the alphabet is ordered by the case priority entered. + +```ts +Alphabet.generateFrom('aAbBcdCD') +.prioritizeCase('uppercase') +// Returns 'AaBbCDcd' +```` + +#### - `placeAllWithCaseBeforeAllWithOtherCase(caseToComeFirst: 'uppercase' | 'lowercase'): this` + +Permutes characters with cases so that all characters with the entered case are put before the other characters. + +```ts +Alphabet.generateFrom('aAbBcCdD') +.placeAllWithCaseBeforeAllWithOtherCase('lowercase') +// Returns 'abcdABCD' +```` + +#### - `placeCharacterBefore({ characterBefore: string; characterAfter: string }): this` + +Places a specific character right before another character in the alphabet. + +```ts +Alphabet.generateFrom('ab-cd/') +.placeCharacterBefore({ characterBefore: '/', characterAfter: '-' }) +// Returns 'ab/-cd' +``` + +#### - `placeCharacterAfter({ characterBefore: string; characterAfter: string }): this` + +Places a specific character right after another character in the alphabet. + +```ts +Alphabet.generateFrom('ab-cd/') +.placeCharacterAfter({ characterBefore: '/', characterAfter: '-' }) +// Returns 'abcd/-' +``` + +#### - `getCharacters(): string` + +Retrieves the characters from the alphabet. diff --git a/docs/content/guide/getting-started.mdx b/docs/content/guide/getting-started.mdx index c10fbe0b..3f8e528b 100644 --- a/docs/content/guide/getting-started.mdx +++ b/docs/content/guide/getting-started.mdx @@ -140,8 +140,9 @@ The highest priority is given to the settings of a particular rule. Next comes t In settings you can set the following options: -- `type` — The type of sorting. Possible values are `'alphabetical'`, `'natural'` and `'line-length'`. +- `type` — The type of sorting. Possible values are `'alphabetical'`, `'natural'`, `'line-length'` and `custom`. - `order` — The order of sorting. Possible values are `'asc'` and `'desc'`. +- `alphabet` — The custom alphabet to use when `type` is `custom`. - `ignoreCase` — Ignore case when sorting. - `ignorePattern` — Ignore sorting for elements that match the pattern. - `specialCharacters` — Control whether special characters should be kept, trimmed or removed before sorting. Values can be `'keep'`, `'trim'` or `'remove'`. diff --git a/docs/content/rules/sort-array-includes.mdx b/docs/content/rules/sort-array-includes.mdx index 48434a31..c0a3f167 100644 --- a/docs/content/rules/sort-array-includes.mdx +++ b/docs/content/rules/sort-array-includes.mdx @@ -128,6 +128,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -138,6 +139,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-classes.mdx b/docs/content/rules/sort-classes.mdx index 5065aae8..e3da41d6 100644 --- a/docs/content/rules/sort-classes.mdx +++ b/docs/content/rules/sort-classes.mdx @@ -163,6 +163,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -173,6 +174,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-decorators.mdx b/docs/content/rules/sort-decorators.mdx index f8519e57..62e31635 100644 --- a/docs/content/rules/sort-decorators.mdx +++ b/docs/content/rules/sort-decorators.mdx @@ -127,6 +127,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -137,6 +138,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-enums.mdx b/docs/content/rules/sort-enums.mdx index d8b58833..de6cd858 100644 --- a/docs/content/rules/sort-enums.mdx +++ b/docs/content/rules/sort-enums.mdx @@ -93,6 +93,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -103,6 +104,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-exports.mdx b/docs/content/rules/sort-exports.mdx index 12b19537..6968ade5 100644 --- a/docs/content/rules/sort-exports.mdx +++ b/docs/content/rules/sort-exports.mdx @@ -87,6 +87,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -97,6 +98,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-heritage-clauses.mdx b/docs/content/rules/sort-heritage-clauses.mdx index 5a0ab529..2ef7e777 100644 --- a/docs/content/rules/sort-heritage-clauses.mdx +++ b/docs/content/rules/sort-heritage-clauses.mdx @@ -70,6 +70,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -80,6 +81,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-imports.mdx b/docs/content/rules/sort-imports.mdx index 057a4588..320f1e1b 100644 --- a/docs/content/rules/sort-imports.mdx +++ b/docs/content/rules/sort-imports.mdx @@ -126,6 +126,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -136,6 +137,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-interfaces.mdx b/docs/content/rules/sort-interfaces.mdx index b62f3cbb..2778809b 100644 --- a/docs/content/rules/sort-interfaces.mdx +++ b/docs/content/rules/sort-interfaces.mdx @@ -142,6 +142,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -152,6 +153,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` @@ -273,10 +284,10 @@ interface User { firstName: string // unknown lastName: string // unknown username: string // unknown - job: { // multiline + job: { // multiline-member // Stuff about job } - localization: { // multiline + localization: { // multiline-member // Stuff about localization } } @@ -289,7 +300,7 @@ interface User { groups: [ 'unknown', 'method', - 'multiline', + 'multiline-member', ] } ``` @@ -395,7 +406,7 @@ Current API: type: `Array` -default: `{}` +default: `[]` You can define your own groups and use regexp patterns to match specific interface members. @@ -418,7 +429,7 @@ interface User { age: number // unknown isAdmin: boolean // unknown lastUpdated_metadata: Date // bottom - localization?: { // multiline + localization?: { // optional-multiline-member // Stuff about localization } version_metadata: string // bottom @@ -432,7 +443,7 @@ interface User { groups: [ + 'top', // [!code ++] 'unknown', -+ ['optional-multiline', 'bottom'] // [!code ++] ++ ['optional-multiline-member', 'bottom'] // [!code ++] ], + customGroups: [ // [!code ++] + { // [!code ++] @@ -477,7 +488,7 @@ interface User { newlinesBetween: 'ignore', groupKind: 'mixed', groups: [], - customGroups: {}, + customGroups: [], }, ], }, @@ -508,7 +519,7 @@ interface User { newlinesBetween: 'ignore', groupKind: 'mixed', groups: [], - customGroups: {}, + customGroups: [], }, ], }, diff --git a/docs/content/rules/sort-intersection-types.mdx b/docs/content/rules/sort-intersection-types.mdx index 90f5b333..b56f6d78 100644 --- a/docs/content/rules/sort-intersection-types.mdx +++ b/docs/content/rules/sort-intersection-types.mdx @@ -83,6 +83,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -93,6 +94,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-jsx-props.mdx b/docs/content/rules/sort-jsx-props.mdx index 3b030dcf..4fd7b6a7 100644 --- a/docs/content/rules/sort-jsx-props.mdx +++ b/docs/content/rules/sort-jsx-props.mdx @@ -143,6 +143,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -153,6 +154,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-maps.mdx b/docs/content/rules/sort-maps.mdx index febb1e5e..acc41f3c 100644 --- a/docs/content/rules/sort-maps.mdx +++ b/docs/content/rules/sort-maps.mdx @@ -88,6 +88,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -98,6 +99,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-modules.mdx b/docs/content/rules/sort-modules.mdx index 2a57d71e..1db7aa7c 100644 --- a/docs/content/rules/sort-modules.mdx +++ b/docs/content/rules/sort-modules.mdx @@ -186,6 +186,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -196,6 +197,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-named-exports.mdx b/docs/content/rules/sort-named-exports.mdx index 6ba10b2d..e96fddef 100644 --- a/docs/content/rules/sort-named-exports.mdx +++ b/docs/content/rules/sort-named-exports.mdx @@ -106,6 +106,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -116,6 +117,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-named-imports.mdx b/docs/content/rules/sort-named-imports.mdx index d7fb3a3f..fddb8fd5 100644 --- a/docs/content/rules/sort-named-imports.mdx +++ b/docs/content/rules/sort-named-imports.mdx @@ -107,6 +107,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -117,6 +118,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-object-types.mdx b/docs/content/rules/sort-object-types.mdx index afce1585..6502ba45 100644 --- a/docs/content/rules/sort-object-types.mdx +++ b/docs/content/rules/sort-object-types.mdx @@ -104,6 +104,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -114,6 +115,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` @@ -238,10 +249,10 @@ type User = { firstName: string // unknown lastName: string // unknown username: string // unknown - job: { // multiline + job: { // multiline-member // Stuff about job } - localization: { // multiline + localization: { // multiline-member // Stuff about localization } } @@ -360,7 +371,7 @@ Current API: type: `Array` -default: `{}` +default: `[]` You can define your own groups and use regexp pattern to match specific object type members. @@ -383,7 +394,7 @@ type User = { age: number // unknown isAdmin: boolean // unknown lastUpdated_metadata: Date // bottom - localization?: { // multiline + localization?: { // optional-multiline-member // Stuff about localization } version_metadata: string // bottom @@ -397,7 +408,7 @@ type User = { groups: [ + 'top', // [!code ++] 'unknown', -+ ['optional-multiline', 'bottom'] // [!code ++] ++ ['optional-multiline-member', 'bottom'] // [!code ++] ], + customGroups: [ // [!code ++] + { // [!code ++] @@ -441,7 +452,7 @@ type User = { partitionByNewLine: false, newlinesBetween: 'ignore', groups: [], - customGroups: {}, + customGroups: [], }, ], }, @@ -471,7 +482,7 @@ type User = { partitionByNewLine: false, newlinesBetween: 'ignore', groups: [], - customGroups: {}, + customGroups: [], }, ], }, diff --git a/docs/content/rules/sort-objects.mdx b/docs/content/rules/sort-objects.mdx index b509d2f3..2a824846 100644 --- a/docs/content/rules/sort-objects.mdx +++ b/docs/content/rules/sort-objects.mdx @@ -162,6 +162,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -172,6 +173,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-sets.mdx b/docs/content/rules/sort-sets.mdx index b20e93b5..99bc8d87 100644 --- a/docs/content/rules/sort-sets.mdx +++ b/docs/content/rules/sort-sets.mdx @@ -134,6 +134,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -144,6 +145,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-switch-case.mdx b/docs/content/rules/sort-switch-case.mdx index 455618b0..070f549d 100644 --- a/docs/content/rules/sort-switch-case.mdx +++ b/docs/content/rules/sort-switch-case.mdx @@ -155,6 +155,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -165,6 +166,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-union-types.mdx b/docs/content/rules/sort-union-types.mdx index 162f4410..f331603c 100644 --- a/docs/content/rules/sort-union-types.mdx +++ b/docs/content/rules/sort-union-types.mdx @@ -103,6 +103,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -113,6 +114,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/content/rules/sort-variable-declarations.mdx b/docs/content/rules/sort-variable-declarations.mdx index ed2bc97e..c930f91d 100644 --- a/docs/content/rules/sort-variable-declarations.mdx +++ b/docs/content/rules/sort-variable-declarations.mdx @@ -87,6 +87,7 @@ Specifies the sorting method. - `'alphabetical'` — Sort items alphabetically (e.g., “a” < “b” < “c”) using [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). - `'natural'` — Sort items in a [natural](https://github.com/yobacca/natural-orderby) order (e.g., “item2” < “item10”). - `'line-length'` — Sort items by the length of the code line (shorter lines first). +- `'custom'` — Sort items using the alphabet entered in the [`alphabet`](#alphabet) option. ### order @@ -97,6 +98,16 @@ Determines whether the sorted items should be in ascending or descending order. - `'asc'` — Sort items in ascending order (A to Z, 1 to 9). - `'desc'` — Sort items in descending order (Z to A, 9 to 1). +### alphabet + +default: `''` + +Only used when the [`type`](#type) option is set to `'custom'`. Specifies the custom alphabet to use when sorting. + +Use the `Alphabet` utility class from `eslint-plugin-perfectionist/alphabet` to quickly generate a custom alphabet. + +Example: `0123456789abcdef...` + ### ignoreCase default: `true` diff --git a/docs/pages/configs/index.astro b/docs/pages/configs/index.astro index ff6ec5c4..9e545b63 100644 --- a/docs/pages/configs/index.astro +++ b/docs/pages/configs/index.astro @@ -19,6 +19,11 @@ let configs = [ url: '/configs/recommended-line-length', name: 'recommended-line-length', }, + { + description: 'All plugin rules with sorting by your own custom order', + url: '/configs/recommended-custom', + name: 'recommended-custom', + }, ] --- diff --git a/docs/utils/pages.ts b/docs/utils/pages.ts index 5d1b8516..a922d734 100644 --- a/docs/utils/pages.ts +++ b/docs/utils/pages.ts @@ -46,6 +46,10 @@ export let pages: ({ links: Page[] } & Page)[] = [ url: '/configs/recommended-line-length', title: 'recommended-line-length', }, + { + url: '/configs/recommended-custom', + title: 'recommended-custom', + }, ], title: 'Configs', url: '/configs', diff --git a/index.ts b/index.ts index c69cf4a2..0d47ebbe 100644 --- a/index.ts +++ b/index.ts @@ -48,15 +48,17 @@ interface PluginConfig { 'recommended-alphabetical-legacy': Linter.LegacyConfig 'recommended-line-length-legacy': Linter.LegacyConfig 'recommended-natural-legacy': Linter.LegacyConfig + 'recommended-custom-legacy': Linter.LegacyConfig 'recommended-alphabetical': Linter.Config 'recommended-line-length': Linter.Config 'recommended-natural': Linter.Config + 'recommended-custom': Linter.Config } name: string } interface BaseOptions { - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' order: 'desc' | 'asc' } @@ -123,6 +125,10 @@ export default { type: 'natural', order: 'asc', }), + 'recommended-custom-legacy': createLegacyConfig({ + type: 'custom', + order: 'asc', + }), 'recommended-alphabetical': createConfig({ type: 'alphabetical', order: 'asc', @@ -135,5 +141,9 @@ export default { type: 'natural', order: 'asc', }), + 'recommended-custom': createConfig({ + type: 'custom', + order: 'asc', + }), }, } as PluginConfig diff --git a/package.json b/package.json index e46a6de3..facb2d78 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./alphabet": { + "types": "./dist/alphabet.d.ts", + "default": "./dist/utils/alphabet.js" + }, "./package.json": "./package.json" }, "files": [ diff --git a/readme.md b/readme.md index 63dc618c..713e42ec 100644 --- a/readme.md +++ b/readme.md @@ -165,6 +165,7 @@ module.exports = { | [recommended-alphabetical](https://perfectionist.dev/configs/recommended-alphabetical) | All plugin rules with alphabetical sorting in ascending order | | [recommended-natural](https://perfectionist.dev/configs/recommended-natural) | All plugin rules with natural sorting in ascending order | | [recommended-line-length](https://perfectionist.dev/configs/recommended-line-length) | All plugin rules with sorting by line length in descending order | +| [recommended-custom](https://perfectionist.dev/configs/recommended-custom) | All plugin rules with sorting by your own custom order | ## Rules @@ -201,15 +202,15 @@ module.exports = { ### Can I automatically fix problems in the editor? -Yes. To do this, you need to enable autofix in ESLint when you save the file in your editor. Instructions for your editor can be found [here](https://perfectionist.dev/guide/integrations). +Yes. To do this, you need to enable autofix in ESLint when you save the file in your editor. You may find instructions for your editor can be found [here](https://perfectionist.dev/guide/integrations). ### Is it safe? -On the whole, yes. We are very careful to make sure that the work of the plugin does not negatively affect the work of the code. For example, the plugin takes into account spread operators in JSX and objects, comments to the code. Safety is our priority. If you encounter any problem, you can create an [issue](https://github.com/azat-io/eslint-plugin-perfectionist/issues/new/choose). +Overall, yes. We want to make sure that the work of the plugin does not negatively affect the behavior of the code. For example, the plugin takes into account spread operators in JSX and objects, comments to the code. Safety is our priority. If you encounter any problem, you can create an [issue](https://github.com/azat-io/eslint-plugin-perfectionist/issues/new/choose). ### Why not Prettier? -I love Prettier. However, this is not his area of responsibility. Prettier is used for formatting, and ESLint is also used for styling. For example, changing the order of imports can affect how the code works (console.log calls, fetch, style loading). Prettier should not change the AST. There is a cool article about this: ["The Blurry Line Between Formatting and Style"](https://blog.joshuakgoldberg.com/the-blurry-line-between-formatting-and-style) by **@joshuakgoldberg**. +I love Prettier. However, this is not its area of responsibility. Prettier is used for formatting, and ESLint is also used for styling. For example, changing the order of imports can affect how the code works (console.log calls, fetch, style loading). Prettier should not change the AST. There is a cool article about this: ["The Blurry Line Between Formatting and Style"](https://blog.joshuakgoldberg.com/the-blurry-line-between-formatting-and-style) by **@joshuakgoldberg**. ## Versioning Policy diff --git a/rules/sort-array-includes.ts b/rules/sort-array-includes.ts index eb30651f..b7dbb575 100644 --- a/rules/sort-array-includes.ts +++ b/rules/sort-array-includes.ts @@ -8,6 +8,7 @@ import { partitionByCommentJsonSchema, specialCharactersJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, orderJsonSchema, typeJsonSchema, @@ -30,14 +31,15 @@ import { pairwise } from '../utils/pairwise' export type Options = [ Partial<{ + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' groupKind: 'literals-first' | 'spreads-first' | 'mixed' - type: 'alphabetical' | 'line-length' | 'natural' partitionByComment: string[] | boolean | string specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -56,6 +58,7 @@ export let defaultOptions: Required = { type: 'alphabetical', ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', } @@ -78,6 +81,7 @@ export let jsonSchema: JSONSchema4 = { }, specialCharacters: specialCharactersJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, order: orderJsonSchema, type: typeJsonSchema, diff --git a/rules/sort-classes.ts b/rules/sort-classes.ts index 31d15061..6fe28edf 100644 --- a/rules/sort-classes.ts +++ b/rules/sort-classes.ts @@ -15,6 +15,7 @@ import { specialCharactersJsonSchema, newlinesBetweenJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, groupsJsonSchema, orderJsonSchema, @@ -25,17 +26,18 @@ import { sortNodesByDependencies, } from '../utils/sort-nodes-by-dependencies' import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration' +import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration' import { singleCustomGroupJsonSchema, allModifiers, allSelectors, } from './sort-classes.types' -import { validateGeneratedGroupsConfiguration } from './validate-generated-groups-configuration' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { getOverloadSignatureGroups, customGroupMatches, } from './sort-classes-utils' -import { getCustomGroupsCompareOptions } from './get-custom-groups-compare-options' +import { getCustomGroupsCompareOptions } from '../utils/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' @@ -104,6 +106,7 @@ let defaultOptions: Required = { ignoreCase: true, customGroups: [], locales: 'en-US', + alphabet: '', order: 'asc', } @@ -116,6 +119,7 @@ export default createEslintRule({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) + validateCustomSortConfiguration(options) validateGeneratedGroupsConfiguration({ customGroups: options.customGroups, modifiers: allModifiers, @@ -123,6 +127,7 @@ export default createEslintRule({ groups: options.groups, }) validateNewlinesAndPartitionConfiguration(options) + let sourceCode = getSourceCode(context) let eslintDisabledLines = getEslintDisabledLines({ ruleName: context.id, @@ -688,6 +693,7 @@ export default createEslintRule({ specialCharacters: specialCharactersJsonSchema, newlinesBetween: newlinesBetweenJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, groups: groupsJsonSchema, order: orderJsonSchema, diff --git a/rules/sort-classes.types.ts b/rules/sort-classes.types.ts index 754865c5..adde1916 100644 --- a/rules/sort-classes.types.ts +++ b/rules/sort-classes.types.ts @@ -8,7 +8,7 @@ import { export type SortClassesOptions = [ Partial<{ - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' partitionByComment: string[] | boolean | string newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' @@ -19,6 +19,7 @@ export type SortClassesOptions = [ groups: (Group[] | Group)[] order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] export type SingleCustomGroup = diff --git a/rules/sort-decorators.ts b/rules/sort-decorators.ts index a87a9606..26e5cea3 100644 --- a/rules/sort-decorators.ts +++ b/rules/sort-decorators.ts @@ -8,11 +8,13 @@ import { specialCharactersJsonSchema, customGroupsJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, groupsJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' @@ -35,8 +37,8 @@ import { pairwise } from '../utils/pairwise' export type Options = [ Partial<{ + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' customGroups: Record - type: 'alphabetical' | 'line-length' | 'natural' partitionByComment: string[] | boolean | string specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable @@ -48,6 +50,7 @@ export type Options = [ sortOnClasses: boolean order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -69,6 +72,7 @@ let defaultOptions: Required[0]> = { ignoreCase: true, customGroups: {}, locales: 'en-US', + alphabet: '', order: 'asc', groups: [], } @@ -111,6 +115,7 @@ export default createEslintRule, MESSAGE_ID>({ specialCharacters: specialCharactersJsonSchema, customGroups: customGroupsJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, groups: groupsJsonSchema, order: orderJsonSchema, @@ -138,7 +143,7 @@ export default createEslintRule, MESSAGE_ID>({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) - + validateCustomSortConfiguration(options) validateGroupsConfiguration( options.groups, ['unknown'], diff --git a/rules/sort-enums.ts b/rules/sort-enums.ts index 7699aae4..8966ed30 100644 --- a/rules/sort-enums.ts +++ b/rules/sort-enums.ts @@ -8,6 +8,7 @@ import { partitionByNewLineJsonSchema, specialCharactersJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, orderJsonSchema, typeJsonSchema, @@ -16,6 +17,7 @@ import { getFirstUnorderedNodeDependentOn, sortNodesByDependencies, } from '../utils/sort-nodes-by-dependencies' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' import { hasPartitionComment } from '../utils/is-partition-comment' @@ -35,7 +37,7 @@ import { pairwise } from '../utils/pairwise' export type Options = [ Partial<{ - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' partitionByComment: string[] | boolean | string specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable @@ -44,6 +46,7 @@ export type Options = [ order: 'desc' | 'asc' sortByValue: boolean ignoreCase: boolean + alphabet: string }>, ] @@ -63,6 +66,7 @@ let defaultOptions: Required = { sortByValue: false, ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', } @@ -79,6 +83,8 @@ export default createEslintRule({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) + validateCustomSortConfiguration(options) + let sourceCode = getSourceCode(context) let eslintDisabledLines = getEslintDisabledLines({ ruleName: context.id, @@ -196,6 +202,7 @@ export default createEslintRule({ : options.type, specialCharacters: options.specialCharacters, ignoreCase: options.ignoreCase, + alphabet: options.alphabet, locales: options.locales, order: options.order, } @@ -274,6 +281,7 @@ export default createEslintRule({ partitionByNewLine: partitionByNewLineJsonSchema, specialCharacters: specialCharactersJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, order: orderJsonSchema, type: typeJsonSchema, diff --git a/rules/sort-exports.ts b/rules/sort-exports.ts index f4ccee2a..04624aaf 100644 --- a/rules/sort-exports.ts +++ b/rules/sort-exports.ts @@ -7,10 +7,12 @@ import { partitionByNewLineJsonSchema, specialCharactersJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' import { hasPartitionComment } from '../utils/is-partition-comment' @@ -27,14 +29,15 @@ import { pairwise } from '../utils/pairwise' type Options = [ Partial<{ + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' groupKind: 'values-first' | 'types-first' | 'mixed' - type: 'alphabetical' | 'line-length' | 'natural' partitionByComment: string[] | boolean | string specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -55,6 +58,7 @@ let defaultOptions: Required = { groupKind: 'mixed', ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', } @@ -63,6 +67,7 @@ export default createEslintRule({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) + validateCustomSortConfiguration(options) let sourceCode = getSourceCode(context) let partitionComment = options.partitionByComment @@ -191,6 +196,7 @@ export default createEslintRule({ partitionByNewLine: partitionByNewLineJsonSchema, specialCharacters: specialCharactersJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, order: orderJsonSchema, type: typeJsonSchema, diff --git a/rules/sort-heritage-clauses.ts b/rules/sort-heritage-clauses.ts index 17370001..b3a5c30b 100644 --- a/rules/sort-heritage-clauses.ts +++ b/rules/sort-heritage-clauses.ts @@ -7,11 +7,13 @@ import { specialCharactersJsonSchema, customGroupsJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, groupsJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' @@ -30,13 +32,14 @@ import { pairwise } from '../utils/pairwise' export type Options = [ Partial<{ + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' customGroups: Record - type: 'alphabetical' | 'line-length' | 'natural' specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable groups: (Group[] | Group)[] order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -52,6 +55,7 @@ let defaultOptions: Required[0]> = { ignoreCase: true, customGroups: {}, locales: 'en-US', + alphabet: '', order: 'asc', groups: [], } @@ -64,6 +68,7 @@ export default createEslintRule, MESSAGE_ID>({ specialCharacters: specialCharactersJsonSchema, customGroups: customGroupsJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, groups: groupsJsonSchema, order: orderJsonSchema, @@ -91,7 +96,7 @@ export default createEslintRule, MESSAGE_ID>({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) - + validateCustomSortConfiguration(options) validateGroupsConfiguration( options.groups, ['unknown'], diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts index 9c9f889b..21bf981d 100644 --- a/rules/sort-imports.ts +++ b/rules/sort-imports.ts @@ -10,12 +10,14 @@ import { specialCharactersJsonSchema, newlinesBetweenJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, groupsJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { readClosestTsConfigByPath } from '../utils/read-closest-ts-config-by-path' import { getOptionsWithCleanGroups } from '../utils/get-options-with-clean-groups' @@ -46,7 +48,7 @@ export type Options = [ value?: Record type?: Record } - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' partitionByComment: string[] | boolean | string newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' @@ -60,6 +62,7 @@ export type Options = [ maxLineLength?: number order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -122,6 +125,7 @@ export default createEslintRule, MESSAGE_ID>({ environment: 'node', ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', } as const), ) @@ -153,6 +157,7 @@ export default createEslintRule, MESSAGE_ID>({ ...Object.keys(options.customGroups.value ?? {}), ], ) + validateCustomSortConfiguration(options) validateNewlinesAndPartitionConfiguration(options) let tsConfigOutput = options.tsconfigRootDir @@ -629,6 +634,7 @@ export default createEslintRule, MESSAGE_ID>({ specialCharacters: specialCharactersJsonSchema, newlinesBetween: newlinesBetweenJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, groups: groupsJsonSchema, order: orderJsonSchema, @@ -710,6 +716,7 @@ export default createEslintRule, MESSAGE_ID>({ environment: 'node', ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', }, ], diff --git a/rules/sort-interfaces.ts b/rules/sort-interfaces.ts index 5eb6bf10..6267fcf9 100644 --- a/rules/sort-interfaces.ts +++ b/rules/sort-interfaces.ts @@ -22,6 +22,7 @@ let defaultOptions: Required = { ignoreCase: true, customGroups: {}, locales: 'en-US', + alphabet: '', order: 'asc', groups: [], } diff --git a/rules/sort-intersection-types.ts b/rules/sort-intersection-types.ts index 130fc337..fe14c2f9 100644 --- a/rules/sort-intersection-types.ts +++ b/rules/sort-intersection-types.ts @@ -6,12 +6,14 @@ import { specialCharactersJsonSchema, newlinesBetweenJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, groupsJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' @@ -34,7 +36,7 @@ import { pairwise } from '../utils/pairwise' type Options = [ Partial<{ - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' partitionByComment: string[] | boolean | string newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' @@ -43,6 +45,7 @@ type Options = [ partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -75,6 +78,7 @@ let defaultOptions: Required = { type: 'alphabetical', ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', groups: [], } @@ -85,7 +89,7 @@ export default createEslintRule({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) - + validateCustomSortConfiguration(options) validateGroupsConfiguration( options.groups, [ @@ -297,6 +301,7 @@ export default createEslintRule({ specialCharacters: specialCharactersJsonSchema, newlinesBetween: newlinesBetweenJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, groups: groupsJsonSchema, order: orderJsonSchema, diff --git a/rules/sort-jsx-props.ts b/rules/sort-jsx-props.ts index 157b4b96..b667072f 100644 --- a/rules/sort-jsx-props.ts +++ b/rules/sort-jsx-props.ts @@ -6,11 +6,13 @@ import { specialCharactersJsonSchema, customGroupsJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, groupsJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' @@ -29,14 +31,15 @@ import { matches } from '../utils/matches' type Options = [ Partial<{ + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' customGroups: Record - type: 'alphabetical' | 'line-length' | 'natural' specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable groups: (Group[] | Group)[] ignorePattern: string[] order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -55,6 +58,7 @@ let defaultOptions: Required[0]> = { ignoreCase: true, customGroups: {}, locales: 'en-US', + alphabet: '', order: 'asc', groups: [], } @@ -68,6 +72,7 @@ export default createEslintRule, MESSAGE_ID>({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) + validateCustomSortConfiguration(options) validateGroupsConfiguration( options.groups, ['multiline', 'shorthand', 'unknown'], @@ -199,6 +204,7 @@ export default createEslintRule, MESSAGE_ID>({ specialCharacters: specialCharactersJsonSchema, customGroups: customGroupsJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, groups: groupsJsonSchema, order: orderJsonSchema, diff --git a/rules/sort-maps.ts b/rules/sort-maps.ts index 5d24a02c..84532115 100644 --- a/rules/sort-maps.ts +++ b/rules/sort-maps.ts @@ -7,10 +7,12 @@ import { partitionByNewLineJsonSchema, specialCharactersJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' import { hasPartitionComment } from '../utils/is-partition-comment' @@ -29,13 +31,14 @@ import { pairwise } from '../utils/pairwise' type Options = [ Partial<{ - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' partitionByComment: string[] | boolean | string specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -48,6 +51,7 @@ let defaultOptions: Required = { type: 'alphabetical', ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', } @@ -69,6 +73,8 @@ export default createEslintRule({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) + validateCustomSortConfiguration(options) + let sourceCode = getSourceCode(context) let eslintDisabledLines = getEslintDisabledLines({ ruleName: context.id, @@ -192,6 +198,7 @@ export default createEslintRule({ partitionByNewLine: partitionByNewLineJsonSchema, specialCharacters: specialCharactersJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, order: orderJsonSchema, type: typeJsonSchema, diff --git a/rules/sort-modules.ts b/rules/sort-modules.ts index 31501abf..d1c51131 100644 --- a/rules/sort-modules.ts +++ b/rules/sort-modules.ts @@ -17,6 +17,7 @@ import { specialCharactersJsonSchema, newlinesBetweenJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, groupsJsonSchema, orderJsonSchema, @@ -27,13 +28,14 @@ import { sortNodesByDependencies, } from '../utils/sort-nodes-by-dependencies' import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration' +import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-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 { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' +import { getCustomGroupsCompareOptions } from '../utils/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' @@ -93,6 +95,7 @@ let defaultOptions: Required = { ignoreCase: true, customGroups: [], locales: 'en-US', + alphabet: '', order: 'asc', } @@ -113,6 +116,7 @@ export default createEslintRule({ specialCharacters: specialCharactersJsonSchema, newlinesBetween: newlinesBetweenJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, groups: groupsJsonSchema, order: orderJsonSchema, @@ -144,6 +148,7 @@ export default createEslintRule({ create: context => { let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) + validateCustomSortConfiguration(options) validateGeneratedGroupsConfiguration({ customGroups: options.customGroups, modifiers: allModifiers, @@ -151,6 +156,7 @@ export default createEslintRule({ groups: options.groups, }) validateNewlinesAndPartitionConfiguration(options) + let sourceCode = getSourceCode(context) let eslintDisabledLines = getEslintDisabledLines({ ruleName: context.id, diff --git a/rules/sort-modules.types.ts b/rules/sort-modules.types.ts index 20b33709..838c2ef5 100644 --- a/rules/sort-modules.types.ts +++ b/rules/sort-modules.types.ts @@ -8,7 +8,7 @@ import { export type SortModulesOptions = [ Partial<{ - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' partitionByComment: string[] | boolean | string newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' @@ -18,6 +18,7 @@ export type SortModulesOptions = [ partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] export type SingleCustomGroup = ( diff --git a/rules/sort-named-exports.ts b/rules/sort-named-exports.ts index 8aafe7cb..de586374 100644 --- a/rules/sort-named-exports.ts +++ b/rules/sort-named-exports.ts @@ -7,10 +7,12 @@ import { partitionByNewLineJsonSchema, specialCharactersJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' import { hasPartitionComment } from '../utils/is-partition-comment' @@ -28,14 +30,15 @@ import { pairwise } from '../utils/pairwise' type Options = [ Partial<{ + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' groupKind: 'values-first' | 'types-first' | 'mixed' - type: 'alphabetical' | 'line-length' | 'natural' partitionByComment: string[] | boolean | string specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -54,6 +57,7 @@ let defaultOptions: Required = { groupKind: 'mixed', ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', } @@ -66,6 +70,8 @@ export default createEslintRule({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) + validateCustomSortConfiguration(options) + let sourceCode = getSourceCode(context) let eslintDisabledLines = getEslintDisabledLines({ ruleName: context.id, @@ -192,6 +198,7 @@ export default createEslintRule({ partitionByNewLine: partitionByNewLineJsonSchema, specialCharacters: specialCharactersJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, order: orderJsonSchema, type: typeJsonSchema, diff --git a/rules/sort-named-imports.ts b/rules/sort-named-imports.ts index a259d3b3..ec17f4fd 100644 --- a/rules/sort-named-imports.ts +++ b/rules/sort-named-imports.ts @@ -7,10 +7,12 @@ import { partitionByNewLineJsonSchema, specialCharactersJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' import { hasPartitionComment } from '../utils/is-partition-comment' @@ -28,8 +30,8 @@ import { pairwise } from '../utils/pairwise' type Options = [ Partial<{ + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' groupKind: 'values-first' | 'types-first' | 'mixed' - type: 'alphabetical' | 'line-length' | 'natural' partitionByComment: string[] | boolean | string specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable @@ -37,6 +39,7 @@ type Options = [ order: 'desc' | 'asc' ignoreAlias: boolean ignoreCase: boolean + alphabet: string }>, ] @@ -56,6 +59,7 @@ let defaultOptions: Required = { groupKind: 'mixed', ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', } @@ -71,6 +75,8 @@ export default createEslintRule({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) + validateCustomSortConfiguration(options) + let sourceCode = getSourceCode(context) let eslintDisabledLines = getEslintDisabledLines({ ruleName: context.id, @@ -205,6 +211,7 @@ export default createEslintRule({ partitionByNewLine: partitionByNewLineJsonSchema, specialCharacters: specialCharactersJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, order: orderJsonSchema, type: typeJsonSchema, diff --git a/rules/sort-object-types.ts b/rules/sort-object-types.ts index 95c6647a..cbd5d690 100644 --- a/rules/sort-object-types.ts +++ b/rules/sort-object-types.ts @@ -13,14 +13,16 @@ import { newlinesBetweenJsonSchema, customGroupsJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, groupsJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration' -import { validateGeneratedGroupsConfiguration } from './validate-generated-groups-configuration' -import { getCustomGroupsCompareOptions } from './get-custom-groups-compare-options' +import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' +import { getCustomGroupsCompareOptions } from '../utils/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' @@ -74,6 +76,7 @@ let defaultOptions: Required = { ignoreCase: true, customGroups: {}, locales: 'en-US', + alphabet: '', order: 'asc', groups: [], } @@ -108,6 +111,7 @@ export let jsonSchema: JSONSchema4 = { specialCharacters: specialCharactersJsonSchema, newlinesBetween: newlinesBetweenJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, groups: groupsJsonSchema, order: orderJsonSchema, @@ -181,6 +185,7 @@ export let sortObjectTypeElements = ({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) + validateCustomSortConfiguration(options) validateGeneratedGroupsConfiguration({ customGroups: options.customGroups, selectors: allSelectors, diff --git a/rules/sort-object-types.types.ts b/rules/sort-object-types.types.ts index 443af3b4..8ce16ddb 100644 --- a/rules/sort-object-types.types.ts +++ b/rules/sort-object-types.types.ts @@ -9,11 +9,11 @@ import { export type Options = [ Partial<{ customGroups: Record | CustomGroup[] + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' /** * @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' @@ -23,6 +23,7 @@ export type Options = [ ignorePattern: string[] order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] diff --git a/rules/sort-objects.ts b/rules/sort-objects.ts index c79643e1..82ea8330 100644 --- a/rules/sort-objects.ts +++ b/rules/sort-objects.ts @@ -10,6 +10,7 @@ import { newlinesBetweenJsonSchema, customGroupsJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, groupsJsonSchema, orderJsonSchema, @@ -20,6 +21,7 @@ import { sortNodesByDependencies, } from '../utils/sort-nodes-by-dependencies' import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { getMatchingContextOptions } from '../utils/get-matching-context-options' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' @@ -48,8 +50,8 @@ type Options = Partial<{ useConfigurationIf: { allNamesMatchPattern?: string } + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' destructuredObjects: { groups: boolean } | boolean - type: 'alphabetical' | 'line-length' | 'natural' customGroups: Record partitionByComment: string[] | boolean | string newlinesBetween: 'ignore' | 'always' | 'never' @@ -66,6 +68,7 @@ type Options = Partial<{ ignorePattern: string[] order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>[] type MESSAGE_ID = @@ -92,6 +95,7 @@ let defaultOptions: Required = { ignoreCase: true, customGroups: {}, locales: 'en-US', + alphabet: '', order: 'asc', groups: [], } @@ -114,6 +118,7 @@ export default createEslintRule({ contextOptions: context.options, }) let options = complete(matchedContextOptions, settings, defaultOptions) + validateCustomSortConfiguration(options) validateGroupsConfiguration( options.groups, ['multiline', 'method', 'unknown'], @@ -541,6 +546,7 @@ export default createEslintRule({ newlinesBetween: newlinesBetweenJsonSchema, customGroups: customGroupsJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, groups: groupsJsonSchema, order: orderJsonSchema, diff --git a/rules/sort-switch-case.ts b/rules/sort-switch-case.ts index edc1a7c6..bf41408b 100644 --- a/rules/sort-switch-case.ts +++ b/rules/sort-switch-case.ts @@ -6,10 +6,12 @@ import type { SortingNode } from '../typings' import { specialCharactersJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { makeCommentAfterFixes } from '../utils/make-comment-after-fixes' import { createEslintRule } from '../utils/create-eslint-rule' import { getSourceCode } from '../utils/get-source-code' @@ -24,11 +26,12 @@ import { compare } from '../utils/compare' type Options = [ Partial<{ - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -43,6 +46,7 @@ let defaultOptions: Required = { type: 'alphabetical', ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', } @@ -56,9 +60,9 @@ export default createEslintRule({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) + validateCustomSortConfiguration(options) let sourceCode = getSourceCode(context) - let isDiscriminantTrue = switchNode.discriminant.type === 'Literal' && switchNode.discriminant.value === true @@ -265,6 +269,7 @@ export default createEslintRule({ properties: { specialCharacters: specialCharactersJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, order: orderJsonSchema, type: typeJsonSchema, diff --git a/rules/sort-union-types.ts b/rules/sort-union-types.ts index e8b45874..8fa1601d 100644 --- a/rules/sort-union-types.ts +++ b/rules/sort-union-types.ts @@ -6,12 +6,14 @@ import { specialCharactersJsonSchema, newlinesBetweenJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, groupsJsonSchema, orderJsonSchema, typeJsonSchema, } from '../utils/common-json-schemas' import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { validateGroupsConfiguration } from '../utils/validate-groups-configuration' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' @@ -34,7 +36,7 @@ import { pairwise } from '../utils/pairwise' type Options = [ Partial<{ - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' partitionByComment: string[] | boolean | string newlinesBetween: 'ignore' | 'always' | 'never' specialCharacters: 'remove' | 'trim' | 'keep' @@ -43,6 +45,7 @@ type Options = [ partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -75,6 +78,7 @@ let defaultOptions: Required = { type: 'alphabetical', ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', groups: [], } @@ -85,7 +89,7 @@ export default createEslintRule({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) - + validateCustomSortConfiguration(options) validateGroupsConfiguration( options.groups, [ @@ -296,6 +300,7 @@ export default createEslintRule({ specialCharacters: specialCharactersJsonSchema, newlinesBetween: newlinesBetweenJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, groups: groupsJsonSchema, order: orderJsonSchema, diff --git a/rules/sort-variable-declarations.ts b/rules/sort-variable-declarations.ts index 25e47770..a1d04d42 100644 --- a/rules/sort-variable-declarations.ts +++ b/rules/sort-variable-declarations.ts @@ -7,6 +7,7 @@ import { partitionByNewLineJsonSchema, specialCharactersJsonSchema, ignoreCaseJsonSchema, + alphabetJsonSchema, localesJsonSchema, orderJsonSchema, typeJsonSchema, @@ -15,6 +16,7 @@ import { getFirstUnorderedNodeDependentOn, sortNodesByDependencies, } from '../utils/sort-nodes-by-dependencies' +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled' import { hasPartitionComment } from '../utils/is-partition-comment' @@ -33,13 +35,14 @@ import { pairwise } from '../utils/pairwise' type Options = [ Partial<{ - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' partitionByComment: string[] | boolean | string specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }>, ] @@ -54,6 +57,7 @@ let defaultOptions: Required = { type: 'alphabetical', ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', } @@ -66,6 +70,8 @@ export default createEslintRule({ let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) + validateCustomSortConfiguration(options) + let sourceCode = getSourceCode(context) let eslintDisabledLines = getEslintDisabledLines({ ruleName: context.id, @@ -282,6 +288,7 @@ export default createEslintRule({ partitionByNewLine: partitionByNewLineJsonSchema, specialCharacters: specialCharactersJsonSchema, ignoreCase: ignoreCaseJsonSchema, + alphabet: alphabetJsonSchema, locales: localesJsonSchema, order: orderJsonSchema, type: typeJsonSchema, diff --git a/test/alphabet.test.ts b/test/alphabet.test.ts new file mode 100644 index 00000000..ecc1846e --- /dev/null +++ b/test/alphabet.test.ts @@ -0,0 +1,276 @@ +import { describe, expect, it } from 'vitest' + +import { Alphabet } from '../utils/alphabet' + +describe('alphabet', () => { + describe('generators', () => { + describe('generateFrom', () => { + it('allows to generate an alphabet from string', () => { + expect(() => Alphabet.generateFrom('abc')).not.toThrow() + }) + + it('allows to generate an alphabet from array', () => { + expect(() => Alphabet.generateFrom(['a', 'b', 'c'])).not.toThrow() + }) + + it('throws when a duplicate character is found in a string input', () => { + expect(() => Alphabet.generateFrom('aa')).toThrow( + 'The alphabet must not contain repeated characters', + ) + }) + + it('throws when a duplicate character is found in an array input', () => { + expect(() => Alphabet.generateFrom(['a', 'a'])).toThrow( + 'The alphabet must not contain repeated characters', + ) + }) + + it('throws when a non-single character is found in a array input', () => { + expect(() => Alphabet.generateFrom(['ab'])).toThrow( + 'The alphabet must contain single characters', + ) + }) + }) + + describe('generateCompleteAlphabet', () => { + it('allows to generate a complete alphabet', () => { + expect( + Alphabet.generateCompleteAlphabet().getCharacters(), + ).toHaveLength(458752) + }) + }) + + describe('generateRecommendedAlphabet', () => { + it('allows to generate the recommended alphabet', () => { + let charactersThatShouldBeIncluded = [ + 'a', + 'A', + String(1), + '<', + '@', + 'ê', + 'Ê', + '{', + '[', + '_', + '$', + '🙂', + ] + + let generatedCharacters = + Alphabet.generateRecommendedAlphabet().getCharacters() + + expect(generatedCharacters).toHaveLength(196608) + for (let character of charactersThatShouldBeIncluded) { + expect(generatedCharacters).toContain(character) + } + }) + }) + + describe('adding/removing characters', () => { + it('allows to push characters with string input', () => { + expect( + Alphabet.generateFrom('ab').pushCharacters('cdd').getCharacters(), + ).toBe('abcd') + }) + + it('allows to push characters with array input', () => { + expect( + Alphabet.generateFrom('ab') + .pushCharacters(['c', 'd', 'd']) + .getCharacters(), + ).toBe('abcd') + }) + + it('throws when a non-single character is found in a array input', () => { + expect(() => Alphabet.generateFrom('').pushCharacters(['ab'])).toThrow( + 'Only single characters may be pushed', + ) + }) + + it('throws when pushing a character that already exists in the alphabet', () => { + expect(() => Alphabet.generateFrom('ab').pushCharacters('ab')).toThrow( + 'The alphabet already contains the characters a, b', + ) + }) + + it('allows removing characters', () => { + expect( + Alphabet.generateFrom('ab').removeCharacters('aac').getCharacters(), + ).toBe('b') + }) + + it('allows removing a Unicode range', () => { + expect( + Alphabet.generateCompleteAlphabet() + .removeUnicodeRange({ start: 0, end: 9 }) + .getCharacters(), + ).toHaveLength(458742) + }) + }) + + describe('sorters', () => { + it('allows sorting by localeCompare', () => { + expect( + Alphabet.generateFrom('bac').sortByLocaleCompare().getCharacters(), + ).toBe('abc') + }) + + it('allows sorting by natural sort', () => { + expect( + Alphabet.generateFrom('1bac2').sortByNaturalSort().getCharacters(), + ).toBe('12abc') + }) + + it('allows sorting by charCodeAt', () => { + expect( + Alphabet.generateFrom('1bac2').sortByCharCodeAt().getCharacters(), + ).toBe('12abc') + }) + + it('allows sorting by custom sorting function', () => { + expect( + Alphabet.generateFrom('1bac2') + .sortBy((a, b) => { + if (a === 'a') { + return 1 + } + if (b === 'a') { + return -1 + } + return 0 + }) + .getCharacters(), + ).toBe('1bc2a') + }) + }) + + describe('misc utilities', () => { + it('allows reversing the alphabet', () => { + expect(Alphabet.generateFrom('abc').reverse().getCharacters()).toBe( + 'cba', + ) + }) + + describe('prioritizeCase', () => { + it('allows to prioritize uppercase', () => { + expect( + Alphabet.generateFrom('aABbc!dCD') + .prioritizeCase('uppercase') + .getCharacters(), + ).toBe('AaBbC!Dcd') + }) + + it('allows to prioritize lowercase', () => { + expect( + Alphabet.generateFrom('aABbcdCDe') + .prioritizeCase('lowercase') + .getCharacters(), + ).toBe('aAbBcdCDe') + }) + }) + + describe('placeCharacterBefore', () => { + it('allows to place a character before another', () => { + expect( + Alphabet.generateFrom('ab-cd/') + .placeCharacterBefore({ + characterBefore: '/', + characterAfter: '-', + }) + .getCharacters(), + ).toBe('ab/-cd') + }) + + it('does nothing if characterBefore is already before characterAfter', () => { + expect( + Alphabet.generateFrom('abcd') + .placeCharacterBefore({ + characterBefore: 'b', + characterAfter: 'd', + }) + .getCharacters(), + ).toBe('abcd') + }) + + it('throws when the characterBefore is not in the alphabet', () => { + expect(() => + Alphabet.generateFrom('a').placeCharacterBefore({ + characterBefore: 'b', + characterAfter: 'a', + }), + ).toThrow('Character b not found in alphabet') + }) + + it('throws when the characterAfter is not in the alphabet', () => { + expect(() => + Alphabet.generateFrom('a').placeCharacterBefore({ + characterBefore: 'a', + characterAfter: 'b', + }), + ).toThrow('Character b not found in alphabet') + }) + }) + + describe('placeCharacterAfter', () => { + it('allows to place a character after another', () => { + expect( + Alphabet.generateFrom('ab-cd/') + .placeCharacterAfter({ + characterBefore: '/', + characterAfter: '-', + }) + .getCharacters(), + ).toBe('abcd/-') + }) + + it('does nothing if characterBefore is already after characterBefore', () => { + expect( + Alphabet.generateFrom('abcd') + .placeCharacterAfter({ + characterBefore: 'b', + characterAfter: 'd', + }) + .getCharacters(), + ).toBe('abcd') + }) + + it('throws when the characterBefore is not in the alphabet', () => { + expect(() => + Alphabet.generateFrom('a').placeCharacterAfter({ + characterBefore: 'b', + characterAfter: 'a', + }), + ).toThrow('Character b not found in alphabet') + }) + + it('throws when the characterAfter is not in the alphabet', () => { + expect(() => + Alphabet.generateFrom('a').placeCharacterAfter({ + characterBefore: 'a', + characterAfter: 'b', + }), + ).toThrow('Character b not found in alphabet') + }) + }) + + describe('placeAllWithCaseBeforeAllWithOtherCase', () => { + it('allows to placing all uppercase before lowercase ones', () => { + expect( + Alphabet.generateFrom('aAbBcCdD') + .placeAllWithCaseBeforeAllWithOtherCase('uppercase') + .getCharacters(), + ).toBe('ABCDabcd') + }) + + it('allows to placing all lowercase before uppercase ones', () => { + expect( + Alphabet.generateFrom('aAbBcCdD') + .placeAllWithCaseBeforeAllWithOtherCase('lowercase') + .getCharacters(), + ).toBe('abcdABCD') + }) + }) + }) + }) +}) diff --git a/test/compare.test.ts b/test/compare.test.ts index 245118aa..e2c9445f 100644 --- a/test/compare.test.ts +++ b/test/compare.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import type { SortingNode } from '../typings' +import { Alphabet } from '../utils/alphabet' import { compare } from '../utils/compare' describe('compare', () => { @@ -133,8 +134,126 @@ describe('compare', () => { }) }) + describe('line-length', () => { + let compareOptions = { + specialCharacters: 'keep', + type: 'line-length', + ignoreCase: false, + locales: 'en-US', + order: 'desc', + } as const + + it('sorts by order asc', () => { + expect( + compare(createTestNode({ name: 'b' }), createTestNode({ name: 'aa' }), { + ...compareOptions, + order: 'asc', + }), + ).toBe(-1) + }) + + it('sorts by order desc', () => { + expect( + compare( + createTestNode({ name: 'aa' }), + createTestNode({ name: 'b' }), + compareOptions, + ), + ).toBe(-1) + }) + }) + + describe('custom', () => { + let compareOptions = { + alphabet: Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters(), + specialCharacters: 'keep', + ignoreCase: false, + locales: 'en-US', + type: 'custom', + order: 'asc', + } as const + + it('sorts by order asc', () => { + expect( + compare( + createTestNode({ name: 'b' }), + createTestNode({ name: 'a' }), + compareOptions, + ), + ).toBe(1) + }) + + it('sorts by order desc', () => { + expect( + compare(createTestNode({ name: 'a' }), createTestNode({ name: 'b' }), { + ...compareOptions, + order: 'desc', + }), + ).toBe(1) + }) + + it('sorts ignoring case', () => { + expect( + compare( + createTestNode({ name: 'aB' }), + createTestNode({ name: 'Ab' }), + { + ...compareOptions, + ignoreCase: true, + }, + ), + ).toBe(0) + }) + + it('sorts while trimming special characters', () => { + expect( + compare(createTestNode({ name: '_a' }), createTestNode({ name: 'a' }), { + ...compareOptions, + specialCharacters: 'trim', + }), + ).toBe(0) + }) + + it('sorts while removing special characters', () => { + expect( + compare( + createTestNode({ name: 'ab' }), + createTestNode({ name: 'a_b' }), + { + ...compareOptions, + specialCharacters: 'remove', + }, + ), + ).toBe(0) + }) + + it('gives minimum priority to characters not in the alphabet', () => { + expect( + compare(createTestNode({ name: 'a' }), createTestNode({ name: 'b' }), { + ...compareOptions, + alphabet: 'b', + }), + ).toBe(1) + expect( + compare(createTestNode({ name: 'b' }), createTestNode({ name: 'a' }), { + ...compareOptions, + alphabet: 'b', + }), + ).toBe(-1) + expect( + compare(createTestNode({ name: 'b' }), createTestNode({ name: 'a' }), { + ...compareOptions, + alphabet: 'c', + }), + ).toBe(0) + }) + }) + let createTestNode = ({ name }: { name: string }): SortingNode => ({ + size: name.length, name, }) as SortingNode }) diff --git a/test/get-settings.test.ts b/test/get-settings.test.ts index 482c7a7b..98a7a653 100644 --- a/test/get-settings.test.ts +++ b/test/get-settings.test.ts @@ -27,6 +27,7 @@ describe('get-settings', () => { ignorePattern: [], ignoreCase: true, locales: 'en-US', + alphabet: '', order: 'asc', } expect(() => { diff --git a/test/sort-array-includes.test.ts b/test/sort-array-includes.test.ts index b9bb5cd8..0959acb1 100644 --- a/test/sort-array-includes.test.ts +++ b/test/sort-array-includes.test.ts @@ -6,6 +6,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import rule from '../rules/sort-array-includes' +import { Alphabet } from '../utils/alphabet' let ruleName = 'sort-array-includes' @@ -1329,6 +1330,65 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts arrays`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: 'b', + left: 'c', + }, + messageId: 'unexpectedArrayIncludesOrder', + }, + ], + output: dedent` + [ + 'a', + 'b', + 'c', + 'd', + ].includes(value) + `, + code: dedent` + [ + 'a', + 'c', + 'b', + 'd', + ].includes(value) + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + [ + 'a', + 'b', + 'c', + 'd', + ].includes(value) + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: misc`, () => { ruleTester.run( `${ruleName}: sets alphabetical asc sorting as default`, diff --git a/test/sort-classes.test.ts b/test/sort-classes.test.ts index a0731358..f46766e9 100644 --- a/test/sort-classes.test.ts +++ b/test/sort-classes.test.ts @@ -5,6 +5,7 @@ import { RuleTester as EslintRuleTester } from 'eslint' import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' +import { Alphabet } from '../utils/alphabet' import rule from '../rules/sort-classes' let ruleName = 'sort-classes' @@ -6100,6 +6101,173 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts class members`, rule, { + invalid: [ + { + options: [ + { + ...options, + groups: [ + 'static-property', + 'protected-property', + 'private-property', + 'property', + 'constructor', + 'static-method', + 'static-protected-method', + 'protected-method', + 'private-method', + 'method', + 'unknown', + ], + }, + ], + errors: [ + { + data: { + right: 'd', + left: 'e', + }, + messageId: 'unexpectedClassesOrder', + }, + { + data: { + leftGroup: 'static-method', + rightGroup: 'constructor', + right: 'constructor', + left: 'f', + }, + messageId: 'unexpectedClassesGroupOrder', + }, + ], + output: dedent` + class Class { + static a = 'a' + + protected b = 'b' + + private c = 'c' + + d = 'd' + + e = 'e' + + constructor() {} + + static f() {} + + protected static g() {} + + protected h() {} + + private i() {} + + j() {} + + k() {} + } + `, + code: dedent` + class Class { + static a = 'a' + + protected b = 'b' + + private c = 'c' + + e = 'e' + + d = 'd' + + static f() {} + + constructor() {} + + protected static g() {} + + protected h() {} + + private i() {} + + j() {} + + k() {} + } + `, + }, + ], + valid: [ + { + code: dedent` + class Class { + a + } + `, + options: [options], + }, + { + options: [ + { + ...options, + groups: [ + 'static-property', + 'protected-property', + 'private-property', + 'property', + 'constructor', + 'static-method', + 'static-protected-method', + 'protected-method', + 'private-method', + 'method', + 'unknown', + ], + }, + ], + code: dedent` + class Class { + static a = 'a' + + protected b = 'b' + + private c = 'c' + + d = 'd' + + e = 'e' + + constructor() {} + + static f() {} + + protected static g() {} + + protected h() {} + + private i() {} + + j() {} + + k() {} + } + `, + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-decorators.test.ts b/test/sort-decorators.test.ts index b96d506b..2d66c179 100644 --- a/test/sort-decorators.test.ts +++ b/test/sort-decorators.test.ts @@ -6,6 +6,7 @@ import { RuleTester as EslintRuleTester } from 'eslint' import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' +import { Alphabet } from '../utils/alphabet' import rule from '../rules/sort-decorators' let ruleName = 'sort-decorators' @@ -2197,6 +2198,118 @@ describe(ruleName, () => { ) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts decorators`, rule, { + invalid: [ + { + output: dedent` + @A @B() @C + class Class { + + @A @B() @C + property + + @A @B() @C + accessor field + + @A @B() @C + method( + @A + @B() + @C + parameter) {} + + } + `, + code: dedent` + @A @C @B() + class Class { + + @A @C @B() + property + + @A @C @B() + accessor field + + @A @C @B() + method( + @A + @C + @B() + parameter) {} + + } + `, + errors: duplicate5Times([ + { + data: { + right: 'B', + left: 'C', + }, + messageId: 'unexpectedDecoratorsOrder', + }, + ]), + options: [options], + }, + ], + valid: [ + { + code: dedent` + @A + class Class { + + @A + property + + @A + accessor field + + @A + method( + @A + parameter) {} + + } + `, + options: [options], + }, + { + code: dedent` + @A + @B() + @C + class Class { + + @A @B() C + property + + @A @B() C + accessor field + + @A @B() C + method( + @A @B() @C + parameter) {} + + } + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length' diff --git a/test/sort-enums.test.ts b/test/sort-enums.test.ts index fd8628e5..ca7ffc1f 100644 --- a/test/sort-enums.test.ts +++ b/test/sort-enums.test.ts @@ -4,6 +4,7 @@ import { dedent } from 'ts-dedent' import type { Options } from '../rules/sort-enums' +import { Alphabet } from '../utils/alphabet' import rule from '../rules/sort-enums' let ruleName = 'sort-enums' @@ -1142,6 +1143,65 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts enum members`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: 'bbb', + left: 'cc', + }, + messageId: 'unexpectedEnumsOrder', + }, + ], + output: dedent` + enum Enum { + aaaa = 'a', + bbb = 'b', + cc = 'c', + d = 'd', + } + `, + code: dedent` + enum Enum { + aaaa = 'a', + cc = 'c', + bbb = 'b', + d = 'd', + } + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + enum Enum { + aaaa = 'a', + bbb = 'b', + cc = 'c', + d = 'd', + } + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-exports.test.ts b/test/sort-exports.test.ts index 9425f173..004600b3 100644 --- a/test/sort-exports.test.ts +++ b/test/sort-exports.test.ts @@ -5,6 +5,7 @@ import { RuleTester as EslintRuleTester } from 'eslint' import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' +import { Alphabet } from '../utils/alphabet' import rule from '../rules/sort-exports' let ruleName = 'sort-exports' @@ -878,6 +879,66 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts exports`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: 'a', + left: 'b', + }, + messageId: 'unexpectedExportsOrder', + }, + { + data: { + right: 'c', + left: 'd', + }, + messageId: 'unexpectedExportsOrder', + }, + ], + output: dedent` + export { a1 } from 'a' + export { b1, b2 } from 'b' + export { c1, c2, c3 } from 'c' + export { d1, d2 } from 'd' + `, + code: dedent` + export { b1, b2 } from 'b' + export { a1 } from 'a' + export { d1, d2 } from 'd' + export { c1, c2, c3 } from 'c' + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + export { a1 } from 'a' + export { b1, b2 } from 'b' + export { c1, c2, c3 } from 'c' + export { d1, d2 } from 'd' + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-heritage-clauses.test.ts b/test/sort-heritage-clauses.test.ts index bd3e3d09..32659ca1 100644 --- a/test/sort-heritage-clauses.test.ts +++ b/test/sort-heritage-clauses.test.ts @@ -6,6 +6,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import rule from '../rules/sort-heritage-clauses' +import { Alphabet } from '../utils/alphabet' let ruleName = 'sort-heritage-clauses' @@ -699,6 +700,96 @@ describe(ruleName, () => { ) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts heritage clauses`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: 'b', + left: 'c', + }, + messageId: 'unexpectedHeritageClausesOrder', + }, + ], + output: dedent` + interface Interface extends + a, + b, + c { + } + `, + code: dedent` + interface Interface extends + a, + c, + b { + } + `, + options: [options], + }, + { + errors: [ + { + data: { + right: 'b', + left: 'c', + }, + messageId: 'unexpectedHeritageClausesOrder', + }, + ], + output: dedent` + interface Interface extends + A.a, + B.b, + C.c { + } + `, + code: dedent` + interface Interface extends + A.a, + C.c, + B.b { + } + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + interface Interface extends + a, + b, + c { + } + `, + options: [options], + }, + { + code: dedent` + interface Interface extends + a { + } + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-imports.test.ts b/test/sort-imports.test.ts index e624f35c..e2f672ab 100644 --- a/test/sort-imports.test.ts +++ b/test/sort-imports.test.ts @@ -15,6 +15,7 @@ import type { MESSAGE_ID, Options } from '../rules/sort-imports' import * as readClosestTsConfigUtils from '../utils/read-closest-ts-config-by-path' import * as getTypescriptImportUtils from '../utils/get-typescript-import' +import { Alphabet } from '../utils/alphabet' import rule from '../rules/sort-imports' let ruleName = 'sort-imports' @@ -3924,6 +3925,53 @@ describe(ruleName, () => { ) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts imports`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: 'a', + left: 'b', + }, + messageId: 'unexpectedImportsOrder', + }, + ], + output: dedent` + import { a1, a2 } from 'a' + import { b1 } from 'b' + `, + code: dedent` + import { b1 } from 'b' + import { a1, a2 } from 'a' + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + import { a1, a2 } from 'a' + import { b1 } from 'b' + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-interfaces.test.ts b/test/sort-interfaces.test.ts index ec37d507..3c795207 100644 --- a/test/sort-interfaces.test.ts +++ b/test/sort-interfaces.test.ts @@ -4,6 +4,7 @@ import { dedent } from 'ts-dedent' import type { Options } from '../rules/sort-interfaces' +import { Alphabet } from '../utils/alphabet' import rule from '../rules/sort-interfaces' let ruleName = 'sort-interfaces' @@ -2790,6 +2791,70 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts interface properties`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: 'b', + left: 'c', + }, + messageId: 'unexpectedInterfacePropertiesOrder', + }, + ], + output: dedent` + interface Interface { + a: string + b: 'b1' | 'b2', + c: string + } + `, + code: dedent` + interface Interface { + a: string + c: string + b: 'b1' | 'b2', + } + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + interface Interface { + a: string + } + `, + options: [options], + }, + { + code: dedent` + interface Interface { + a: string + b: 'b1' | 'b2', + c: string + } + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-intersection-types.test.ts b/test/sort-intersection-types.test.ts index cc61cf08..d1cadbb6 100644 --- a/test/sort-intersection-types.test.ts +++ b/test/sort-intersection-types.test.ts @@ -3,6 +3,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import rule from '../rules/sort-intersection-types' +import { Alphabet } from '../utils/alphabet' let ruleName = 'sort-intersection-types' @@ -1316,6 +1317,50 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}: sorts intersection types`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: "{ label: 'bb' }", + left: "{ label: 'c' }", + }, + messageId: 'unexpectedIntersectionTypesOrder', + }, + ], + output: dedent` + type Type = { label: 'aaa' } & { label: 'bb' } & { label: 'c' } + `, + code: dedent` + type Type = { label: 'aaa' } & { label: 'c' } & { label: 'bb' } + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + type Type = { label: 'aaa' } & { label: 'bb' } & { label: 'c' } + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-jsx-props.test.ts b/test/sort-jsx-props.test.ts index d84e960b..675d994b 100644 --- a/test/sort-jsx-props.test.ts +++ b/test/sort-jsx-props.test.ts @@ -4,6 +4,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import path from 'node:path' +import { Alphabet } from '../utils/alphabet' import rule from '../rules/sort-jsx-props' let ruleName = 'sort-jsx-props' @@ -1057,6 +1058,74 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts jsx props`, rule, { + invalid: [ + { + output: dedent` + let Component = () => ( + + Value + + ) + `, + code: dedent` + let Component = () => ( + + Value + + ) + `, + errors: [ + { + data: { + right: 'b', + left: 'c', + }, + messageId: 'unexpectedJSXPropsOrder', + }, + ], + options: [options], + }, + ], + valid: [ + { + code: dedent` + let Component = () => ( + + Value + + ) + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-maps.test.ts b/test/sort-maps.test.ts index b0916d4c..5d2ef751 100644 --- a/test/sort-maps.test.ts +++ b/test/sort-maps.test.ts @@ -5,6 +5,7 @@ import { RuleTester as EslintRuleTester } from 'eslint' import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' +import { Alphabet } from '../utils/alphabet' import rule from '../rules/sort-maps' let ruleName = 'sort-maps' @@ -768,6 +769,59 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): works with variables as keys`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: 'aa', + left: 'b', + }, + messageId: 'unexpectedMapElementsOrder', + }, + ], + output: dedent` + new Map([ + [aa, aa], + [b, b], + ]) + `, + code: dedent` + new Map([ + [b, b], + [aa, aa], + ]) + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + new Map([ + [aa, aa], + [b, b], + ]) + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-modules.test.ts b/test/sort-modules.test.ts index 1517a862..caaf5804 100644 --- a/test/sort-modules.test.ts +++ b/test/sort-modules.test.ts @@ -5,6 +5,7 @@ import { RuleTester as EslintRuleTester } from 'eslint' import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' +import { Alphabet } from '../utils/alphabet' import rule from '../rules/sort-modules' let ruleName = 'sort-modules' @@ -2571,6 +2572,150 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts modules`, rule, { + invalid: [ + { + errors: [ + { + data: { + leftGroup: 'export-interface', + left: 'FindUserInput', + right: 'CacheType', + rightGroup: 'enum', + }, + messageId: 'unexpectedModulesGroupOrder', + }, + { + data: { + rightGroup: 'export-function', + left: 'assertInputIsCorrect', + leftGroup: 'function', + right: 'findUser', + }, + messageId: 'unexpectedModulesGroupOrder', + }, + { + data: { + leftGroup: 'export-function', + right: 'FindAllUsersInput', + rightGroup: 'export-type', + left: 'findUser', + }, + messageId: 'unexpectedModulesGroupOrder', + }, + { + data: { + leftGroup: 'export-function', + left: 'findAllUsers', + rightGroup: 'class', + right: 'Cache', + }, + messageId: 'unexpectedModulesGroupOrder', + }, + ], + output: dedent` + enum CacheType { + ALWAYS = 'ALWAYS', + NEVER = 'NEVER', + } + + export type FindAllUsersInput = { + ids: string[] + cache: CacheType + } + + export type FindAllUsersOutput = FindUserOutput[] + + export interface FindUserInput { + id: string + cache: CacheType + } + + export type FindUserOutput = { + id: string + name: string + age: number + } + + class Cache { + // Some logic + } + + export function findAllUsers(input: FindAllUsersInput): FindAllUsersOutput { + assertInputIsCorrect(input) + return _findUserByIds(input.ids) + } + + export function findUser(input: FindUserInput): FindUserOutput { + assertInputIsCorrect(input) + return _findUserByIds([input.id])[0] + } + + function assertInputIsCorrect(input: FindUserInput | FindAllUsersInput): void { + // Some logic + } + `, + code: dedent` + export interface FindUserInput { + id: string + cache: CacheType + } + + enum CacheType { + ALWAYS = 'ALWAYS', + NEVER = 'NEVER', + } + + export type FindUserOutput = { + id: string + name: string + age: number + } + + function assertInputIsCorrect(input: FindUserInput | FindAllUsersInput): void { + // Some logic + } + + export function findUser(input: FindUserInput): FindUserOutput { + assertInputIsCorrect(input) + return _findUserByIds([input.id])[0] + } + + export type FindAllUsersInput = { + ids: string[] + cache: CacheType + } + + export type FindAllUsersOutput = FindUserOutput[] + + export function findAllUsers(input: FindAllUsersInput): FindAllUsersOutput { + assertInputIsCorrect(input) + return _findUserByIds(input.ids) + } + + class Cache { + // Some logic + } + `, + options: [options], + }, + ], + valid: [], + }) + }) + describe(`${ruleName}: sorting by line-length`, () => { let type = 'line-length' diff --git a/test/sort-named-exports.test.ts b/test/sort-named-exports.test.ts index 778d576e..a80e6094 100644 --- a/test/sort-named-exports.test.ts +++ b/test/sort-named-exports.test.ts @@ -6,6 +6,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import rule from '../rules/sort-named-exports' +import { Alphabet } from '../utils/alphabet' let ruleName = 'sort-named-exports' @@ -668,6 +669,60 @@ describe(ruleName, () => { ) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts named exports`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: 'bb', + left: 'c', + }, + messageId: 'unexpectedNamedExportsOrder', + }, + ], + output: dedent` + export { + aaa, + bb, + c + } + `, + code: dedent` + export { + aaa, + c, + bb + } + `, + options: [options], + }, + ], + valid: [ + { + code: 'export { a }', + options: [options], + }, + { + code: 'export { aaa, bb, c }', + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-named-imports.test.ts b/test/sort-named-imports.test.ts index 55c81d61..a54c2812 100644 --- a/test/sort-named-imports.test.ts +++ b/test/sort-named-imports.test.ts @@ -6,6 +6,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import rule from '../rules/sort-named-imports' +import { Alphabet } from '../utils/alphabet' let ruleName = 'sort-named-imports' @@ -1145,6 +1146,50 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts named imports`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: 'AAA', + left: 'BB', + }, + messageId: 'unexpectedNamedImportsOrder', + }, + ], + output: dedent` + import { AAA, BB, C } from 'module' + `, + code: dedent` + import { BB, AAA, C } from 'module' + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + import { AAA, BB, C } from 'module' + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-object-types.test.ts b/test/sort-object-types.test.ts index 6fb5b8bf..8c8709f0 100644 --- a/test/sort-object-types.test.ts +++ b/test/sort-object-types.test.ts @@ -3,6 +3,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import rule from '../rules/sort-object-types' +import { Alphabet } from '../utils/alphabet' let ruleName = 'sort-object-types' @@ -2412,6 +2413,62 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts type members`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: 'b', + left: 'c', + }, + messageId: 'unexpectedObjectTypesOrder', + }, + ], + output: dedent` + type Type = { + a: 'aaa' + b: 'bb' + c: 'c' + } + `, + code: dedent` + type Type = { + a: 'aaa' + c: 'c' + b: 'bb' + } + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + type Type = { + a: 'aaa' + b: 'bb' + c: 'c' + } + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-objects.test.ts b/test/sort-objects.test.ts index b82eff75..ca7c7ee6 100644 --- a/test/sort-objects.test.ts +++ b/test/sort-objects.test.ts @@ -5,6 +5,7 @@ import { RuleTester as EslintRuleTester } from 'eslint' import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' +import { Alphabet } from '../utils/alphabet' import rule from '../rules/sort-objects' let ruleName = 'sort-objects' @@ -2777,6 +2778,69 @@ describe(ruleName, () => { ) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run( + `${ruleName}(${type}): sorts object with identifier and literal keys`, + rule, + { + invalid: [ + { + errors: [ + { + data: { + right: 'b', + left: 'c', + }, + messageId: 'unexpectedObjectsOrder', + }, + ], + output: dedent` + let Obj = { + a: 'aaaa', + b: 'bbb', + [c]: 'cc', + d: 'd', + } + `, + code: dedent` + let Obj = { + a: 'aaaa', + [c]: 'cc', + b: 'bbb', + d: 'd', + } + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + let Obj = { + a: 'aaaa', + b: 'bbb', + [c]: 'cc', + d: 'd', + } + `, + options: [options], + }, + ], + }, + ) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-sets.test.ts b/test/sort-sets.test.ts index ab3480ae..460f26e3 100644 --- a/test/sort-sets.test.ts +++ b/test/sort-sets.test.ts @@ -5,6 +5,7 @@ import { RuleTester as EslintRuleTester } from 'eslint' import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' +import { Alphabet } from '../utils/alphabet' import rule from '../rules/sort-sets' let ruleName = 'sort-sets' @@ -1005,6 +1006,65 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts sets`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: 'b', + left: 'c', + }, + messageId: 'unexpectedSetsOrder', + }, + ], + output: dedent` + new Set([ + 'a', + 'b', + 'c', + 'd', + ]) + `, + code: dedent` + new Set([ + 'a', + 'c', + 'b', + 'd', + ]) + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + new Set( + 'a', + 'b', + 'c', + 'd', + ).includes(value) + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-switch-case.test.ts b/test/sort-switch-case.test.ts index a5657ba9..61d6343e 100644 --- a/test/sort-switch-case.test.ts +++ b/test/sort-switch-case.test.ts @@ -6,6 +6,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import rule from '../rules/sort-switch-case' +import { Alphabet } from '../utils/alphabet' let ruleName = 'sort-switch-case' @@ -1857,6 +1858,114 @@ describe(ruleName, () => { ) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): works with grouped cases`, rule, { + invalid: [ + { + output: [ + dedent` + switch (value) { + case 'aaaaaa': + return 'primary' + case 'cccc': + case 'ee': + case 'f': + return 'tertiary' + case 'bbbbb': + case 'ddd': + return 'secondary' + case 'x': + default: + return 'unknown' + } + `, + dedent` + switch (value) { + case 'aaaaaa': + return 'primary' + case 'bbbbb': + case 'ddd': + return 'secondary' + case 'cccc': + case 'ee': + case 'f': + return 'tertiary' + case 'x': + default: + return 'unknown' + } + `, + ], + code: dedent` + switch (value) { + case 'aaaaaa': + return 'primary' + case 'ee': + case 'cccc': + case 'f': + return 'tertiary' + case 'bbbbb': + case 'ddd': + return 'secondary' + case 'x': + default: + return 'unknown' + } + `, + errors: [ + { + data: { + right: 'cccc', + left: 'ee', + }, + messageId: 'unexpectedSwitchCaseOrder', + }, + { + data: { + right: 'bbbbb', + left: 'f', + }, + messageId: 'unexpectedSwitchCaseOrder', + }, + ], + options: [options], + }, + ], + valid: [ + { + code: dedent` + switch (value) { + case 'aaaaaa': + return 'primary' + case 'bbbbb': + case 'ddd': + return 'secondary' + case 'cccc': + case 'ee': + case 'f': + return 'tertiary' + case 'x': + default: + return 'unknown' + } + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-union-types.test.ts b/test/sort-union-types.test.ts index 3f7e0631..49129ade 100644 --- a/test/sort-union-types.test.ts +++ b/test/sort-union-types.test.ts @@ -3,6 +3,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import rule from '../rules/sort-union-types' +import { Alphabet } from '../utils/alphabet' let ruleName = 'sort-union-types' @@ -1335,6 +1336,50 @@ describe(ruleName, () => { }) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}: sorts union types`, rule, { + invalid: [ + { + errors: [ + { + data: { + right: "'bbb'", + left: "'cc'", + }, + messageId: 'unexpectedUnionTypesOrder', + }, + ], + output: dedent` + type Type = 'aaaa' | 'bbb' | 'cc' | 'd' + `, + code: dedent` + type Type = 'aaaa' | 'cc' | 'bbb' | 'd' + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + type Type = 'aaaa' | 'bbb' | 'cc' | 'd' + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/sort-variable-declarations.test.ts b/test/sort-variable-declarations.test.ts index 9fcbbd45..686726d5 100644 --- a/test/sort-variable-declarations.test.ts +++ b/test/sort-variable-declarations.test.ts @@ -6,6 +6,7 @@ import { afterAll, describe, it } from 'vitest' import { dedent } from 'ts-dedent' import rule from '../rules/sort-variable-declarations' +import { Alphabet } from '../utils/alphabet' let ruleName = 'sort-variable-declarations' @@ -1287,6 +1288,47 @@ describe(ruleName, () => { ) }) + describe(`${ruleName}: sorts by custom alphabet`, () => { + let type = 'custom' + + let alphabet = Alphabet.generateRecommendedAlphabet() + .sortByLocaleCompare('en-US') + .getCharacters() + let options = { + type: 'custom', + order: 'asc', + alphabet, + } as const + + ruleTester.run(`${ruleName}(${type}): sorts variables declarations`, rule, { + invalid: [ + { + errors: [ + { + messageId: 'unexpectedVariableDeclarationsOrder', + data: { right: 'aaa', left: 'bb' }, + }, + ], + output: dedent` + const aaa, bb, c + `, + code: dedent` + const bb, aaa, c + `, + options: [options], + }, + ], + valid: [ + { + code: dedent` + const aaa, bb, c + `, + options: [options], + }, + ], + }) + }) + describe(`${ruleName}: sorting by line length`, () => { let type = 'line-length-order' diff --git a/test/validate-custom-sort-configuration.test.ts b/test/validate-custom-sort-configuration.test.ts new file mode 100644 index 00000000..aa2587c4 --- /dev/null +++ b/test/validate-custom-sort-configuration.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' + +import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration' + +describe('validate-custom-sort-configuration', () => { + it('accepts empty alphabet when type is not `custom`', () => { + for (let type of ['alphabetical', 'line-length', 'natural'] as const) { + expect(() => + validateCustomSortConfiguration({ + alphabet: '', + type, + }), + ).not.toThrow() + } + }) + + it('throws when an empty alphabet is entered while type is `custom`', () => { + expect(() => + validateCustomSortConfiguration({ + type: 'custom', + alphabet: '', + }), + ).toThrow('alphabet` option must not be empty') + }) +}) diff --git a/test/validate-generated-groups-configuration.test.ts b/test/validate-generated-groups-configuration.test.ts index 11a2284d..e4d38c2a 100644 --- a/test/validate-generated-groups-configuration.test.ts +++ b/test/validate-generated-groups-configuration.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { validateGeneratedGroupsConfiguration } from '../rules/validate-generated-groups-configuration' +import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration' import { allModifiers, allSelectors } from '../rules/sort-classes.types' import { getArrayCombinations } from '../utils/get-array-combinations' diff --git a/utils/alphabet.ts b/utils/alphabet.ts new file mode 100644 index 00000000..f53b2382 --- /dev/null +++ b/utils/alphabet.ts @@ -0,0 +1,434 @@ +import { compare as createNaturalCompare } from 'natural-orderby' + +import { convertBooleanToSign } from './convert-boolean-to-sign' + +interface Character { + uppercaseCharacterCodePoint?: number + lowercaseCharacterCodePoint?: number + codePoint: number + value: string +} + +/** + * Utility class to build alphabets + */ +export class Alphabet { + private _characters: Character[] = [] + + private constructor(characters: Character[]) { + this._characters = characters + } + + /** + * Generates an alphabet from the given characters. + * @param {string|string[]} values - The characters to generate the alphabet from + * @returns {Alphabet} - The wrapped alphabet + */ + public static generateFrom(values: string[] | string): Alphabet { + let arrayValues = typeof values === 'string' ? [...values] : values + if (arrayValues.length !== new Set(arrayValues).size) { + throw new Error('The alphabet must not contain repeated characters') + } + if (arrayValues.some(value => value.length !== 1)) { + throw new Error('The alphabet must contain single characters') + } + return new Alphabet( + arrayValues.map(value => + Alphabet._convertCodepointToCharacter(value.codePointAt(0)!), + ), + ) + } + + /** + * Generates an alphabet containing relevant characters from the Unicode + * standard. Contains the Unicode planes 0 and 1. + * @returns {Alphabet} - The generated alphabet + */ + public static generateRecommendedAlphabet(): Alphabet { + return Alphabet._generateAlphabetToRange(0x1ffff + 1) + } + + /** + * Generates an alphabet containing all characters from the Unicode standard + * except for irrelevant Unicode planes. + * Contains the Unicode planes 0, 1, 2 and 3. + * @returns {Alphabet} - The generated alphabet + */ + public static generateCompleteAlphabet(): Alphabet { + return Alphabet._generateAlphabetToRange(0x3ffff + 1) + } + + private static _convertCodepointToCharacter(codePoint: number): Character { + let character = String.fromCodePoint(codePoint) + let lowercaseCharacter = character.toLowerCase() + let uppercaseCharacter = character.toUpperCase() + return { + value: character, + codePoint, + ...(lowercaseCharacter === character + ? null + : { + lowercaseCharacterCodePoint: lowercaseCharacter.codePointAt(0)!, + }), + ...(uppercaseCharacter === character + ? null + : { + uppercaseCharacterCodePoint: uppercaseCharacter.codePointAt(0)!, + }), + } + } + + /** + * Generates an alphabet containing relevant characters from the Unicode + * standard. + * @param {number} maxCodePoint - The maximum code point to generate the + * alphabet to + * @returns {Alphabet} - The generated alphabet + */ + private static _generateAlphabetToRange(maxCodePoint: number): Alphabet { + let totalCharacters: Character[] = Array.from( + { length: maxCodePoint }, + (_, i) => Alphabet._convertCodepointToCharacter(i), + ) + return new Alphabet(totalCharacters) + } + + /** + * For each character with a lower and upper case, permutes the two cases + * so that the alphabet is ordered by the case priority entered. + * @param {string} casePriority - The case to prioritize + * @returns {Alphabet} - The same alphabet instance with the cases prioritized + * @example + * Alphabet.generateFrom('aAbBcdCD') + * .prioritizeCase('uppercase') + * // Returns 'AaBbCDcd' + */ + public prioritizeCase(casePriority: 'lowercase' | 'uppercase'): this { + let charactersWithCase = this._getCharactersWithCase() + // Permutes each uppercase character with its lowercase one + let parsedIndexes = new Set() + let indexByCodePoints = this._characters.reduce< + Record + >((indexByCodePoint, character, index) => { + indexByCodePoint[character.codePoint] = index + return indexByCodePoint + }, {}) + for (let { character, index } of charactersWithCase) { + if (parsedIndexes.has(index)) { + continue + } + parsedIndexes.add(index) + let otherCharacterIndex = + indexByCodePoints[ + character.uppercaseCharacterCodePoint ?? + character.lowercaseCharacterCodePoint! + ] + // eslint-disable-next-line no-undefined + if (otherCharacterIndex === undefined) { + continue + } + parsedIndexes.add(otherCharacterIndex) + let isCharacterUppercase = !character.uppercaseCharacterCodePoint + if (isCharacterUppercase) { + if ( + (casePriority === 'uppercase' && index < otherCharacterIndex) || + (casePriority === 'lowercase' && index > otherCharacterIndex) + ) { + continue + } + } else { + if ( + (casePriority === 'uppercase' && index > otherCharacterIndex) || + (casePriority === 'lowercase' && index < otherCharacterIndex) + ) { + continue + } + } + this._characters[index] = this._characters[otherCharacterIndex] + this._characters[otherCharacterIndex] = character + } + return this + } + + /** + * Adds specific characters to the end of the alphabet. + * @param {string|string[]} values - The characters to push to the alphabet + * @returns {Alphabet} - The same alphabet instance without the specified + * characters + * @example + * Alphabet.generateFrom('ab') + * .pushCharacters('cd') + * // Returns 'abcd' + */ + public pushCharacters(values: string[] | string): this { + let arrayValues = typeof values === 'string' ? [...values] : values + let valuesSet = new Set(arrayValues) + let valuesAlreadyExisting = this._characters.filter(({ value }) => + valuesSet.has(value), + ) + if (valuesAlreadyExisting.length > 0) { + throw new Error( + `The alphabet already contains the characters ${valuesAlreadyExisting + .slice(0, 5) + .map(({ value }) => value) + .join(', ')}`, + ) + } + if (arrayValues.some(value => value.length !== 1)) { + throw new Error('Only single characters may be pushed') + } + this._characters.push( + ...[...valuesSet].map(value => + Alphabet._convertCodepointToCharacter(value.codePointAt(0)!), + ), + ) + return this + } + + /** + * Permutes characters with cases so that all characters with the entered case + * are put before the other characters. + * @param {string} caseToComeFirst - The case to put before the other + * characters + * @returns {Alphabet} - The same alphabet instance with all characters with + * case before all the characters with the other case + */ + public placeAllWithCaseBeforeAllWithOtherCase( + caseToComeFirst: 'uppercase' | 'lowercase', + ): this { + let charactersWithCase = this._getCharactersWithCase() + let orderedCharacters = [ + ...charactersWithCase.filter(character => + caseToComeFirst === 'uppercase' + ? !character.character.uppercaseCharacterCodePoint + : character.character.uppercaseCharacterCodePoint, + ), + ...charactersWithCase.filter(character => + caseToComeFirst === 'uppercase' + ? character.character.uppercaseCharacterCodePoint + : !character.character.uppercaseCharacterCodePoint, + ), + ] + for (let [i, element] of charactersWithCase.entries()) { + this._characters[element.index] = orderedCharacters[i].character + } + return this + } + + /** + * Places a specific character right before another character in the alphabet. + * @param {object} params - The parameters for the operation + * @param {string} params.characterBefore - The character to come before + * characterAfter + * @param {string} params.characterAfter - The target character + * @returns {Alphabet} - The same alphabet instance with the specific + * character prioritized + * @example + * Alphabet.generateFrom('ab-cd/') + * .placeCharacterBefore({ characterBefore: '/', characterAfter: '-' }) + * // Returns 'ab/-cd' + */ + public placeCharacterBefore({ + characterBefore, + characterAfter, + }: { + characterBefore: string + characterAfter: string + }): this { + return this._placeCharacterBeforeOrAfter({ + characterBefore, + characterAfter, + type: 'before', + }) + } + + /** + * Places a specific character right after another character in the alphabet. + * @param {object} params - The parameters for the operation + * @param {string} params.characterBefore - The target character + * @param {string} params.characterAfter - The character to come after + * characterBefore + * @returns {Alphabet} - The same alphabet instance with the specific + * character prioritized + * @example + * Alphabet.generateFrom('ab-cd/') + * .placeCharacterAfter({ characterBefore: '/', characterAfter: '-' }) + * // Returns 'abcd/-' + */ + public placeCharacterAfter({ + characterBefore, + characterAfter, + }: { + characterBefore: string + characterAfter: string + }): this { + return this._placeCharacterBeforeOrAfter({ + characterBefore, + characterAfter, + type: 'after', + }) + } + + /** + * Removes specific characters from the alphabet by their range. + * @param {object} range - The Unicode range to remove characters from + * @param {number} range.start - The starting Unicode codepoint + * @param {number} range.end - The ending Unicode codepoint + * @returns {Alphabet} - The same alphabet instance without the characters + * from the specified range + */ + public removeUnicodeRange({ + start, + end, + }: { + start: number + end: number + }): this { + this._characters = this._characters.filter( + ({ codePoint }) => codePoint < start || codePoint > end, + ) + return this + } + + /** + * Sorts the alphabet by the sorting function provided. + * @param {Function} sortingFunction - The sorting function to use + * @returns {Alphabet} - The same alphabet instance sorted by the sorting function provided + */ + public sortBy( + sortingFunction: (characterA: string, characterB: string) => number, + ): this { + this._characters.sort((a, b) => sortingFunction(a.value, b.value)) + return this + } + + /** + * Removes specific characters from the alphabet. + * @param {string|string[]} values - The characters to remove from the + * alphabet + * @returns {Alphabet} - The same alphabet instance without the specified + * characters + * @example + * Alphabet.generateFrom('abcd') + * .removeCharacters('dcc') + * // Returns 'ab' + */ + public removeCharacters(values: string[] | string): this { + this._characters = this._characters.filter( + ({ value }) => !values.includes(value), + ) + return this + } + + /** + * Sorts the alphabet by the natural order of the characters using + * `natural-orderby`. + * @param {string} locale - The locale to use for sorting + * @returns {Alphabet} - The same alphabet instance sorted by the natural + * order of the characters + */ + public sortByNaturalSort(locale?: string): this { + let naturalCompare = createNaturalCompare({ + locale, + }) + return this.sortBy((a, b) => naturalCompare(a, b)) + } + + /** + * Sorts the alphabet by the character code point. + * @returns {Alphabet} - The same alphabet instance sorted by the character + * code point + */ + public sortByCharCodeAt(): this { + return this.sortBy((a, b) => + convertBooleanToSign(a.charCodeAt(0) > b.charCodeAt(0)), + ) + } + + /** + * Sorts the alphabet by the locale order of the characters. + * @param {Intl.LocalesArgument} locales - The locales to use for sorting + * @returns {Alphabet} - The same alphabet instance sorted by the locale + * order of the characters + */ + public sortByLocaleCompare(locales?: Intl.LocalesArgument): this { + return this.sortBy((a, b) => a.localeCompare(b, locales)) + } + + /** + * Retrieves the characters from the alphabet. + * @returns {string} The characters from the alphabet + */ + public getCharacters(): string { + return this._characters.map(({ value }) => value).join('') + } + + /** + * Reverses the alphabet. + * @returns {Alphabet} - The same alphabet instance reversed + * @example + * Alphabet.generateFrom('ab') + * .reverse() + * // Returns 'ba' + */ + public reverse(): this { + this._characters.reverse() + return this + } + + private _placeCharacterBeforeOrAfter({ + characterBefore, + characterAfter, + type, + }: { + type: 'before' | 'after' + characterBefore: string + characterAfter: string + }): this { + let indexOfCharacterAfter = this._characters.findIndex( + ({ value }) => value === characterAfter, + ) + let indexOfCharacterBefore = this._characters.findIndex( + ({ value }) => value === characterBefore, + ) + if (indexOfCharacterAfter === -1) { + throw new Error(`Character ${characterAfter} not found in alphabet`) + } + if (indexOfCharacterBefore === -1) { + throw new Error(`Character ${characterBefore} not found in alphabet`) + } + if (indexOfCharacterBefore <= indexOfCharacterAfter) { + return this + } + + this._characters.splice( + type === 'before' ? indexOfCharacterAfter : indexOfCharacterBefore + 1, + 0, + this._characters[ + type === 'before' ? indexOfCharacterBefore : indexOfCharacterAfter + ], + ) + this._characters.splice( + type === 'before' ? indexOfCharacterBefore + 1 : indexOfCharacterAfter, + 1, + ) + return this + } + + private _getCharactersWithCase(): { character: Character; index: number }[] { + return this._characters + .map((character, index) => { + if ( + !character.uppercaseCharacterCodePoint && + !character.lowercaseCharacterCodePoint + ) { + return null + } + return { + character, + index, + } + }) + .filter(element => element !== null) + } +} diff --git a/utils/common-json-schemas.ts b/utils/common-json-schemas.ts index 29564ece..f3698bbc 100644 --- a/utils/common-json-schemas.ts +++ b/utils/common-json-schemas.ts @@ -1,7 +1,7 @@ import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema' export let typeJsonSchema: JSONSchema4 = { - enum: ['alphabetical', 'natural', 'line-length'], + enum: ['alphabetical', 'natural', 'line-length', 'custom'], description: 'Specifies the sorting method.', type: 'string', } @@ -13,6 +13,11 @@ export let orderJsonSchema: JSONSchema4 = { type: 'string', } +export let alphabetJsonSchema: JSONSchema4 = { + description: 'Alphabet to use for the `custom` sort type.', + type: 'string', +} + export let localesJsonSchema: JSONSchema4 = { oneOf: [ { diff --git a/utils/compare.ts b/utils/compare.ts index 38dcf0cf..13aba43c 100644 --- a/utils/compare.ts +++ b/utils/compare.ts @@ -2,10 +2,13 @@ import { compare as createNaturalCompare } from 'natural-orderby' import type { SortingNode } from '../typings' +import { convertBooleanToSign } from './convert-boolean-to-sign' + export type CompareOptions = | AlphabeticalCompareOptions | LineLengthCompareOptions | NaturalCompareOptions + | CustomCompareOptions interface BaseCompareOptions { /** @@ -32,70 +35,135 @@ interface NaturalCompareOptions type: 'natural' } +interface CustomCompareOptions + extends BaseCompareOptions { + specialCharacters: 'remove' | 'trim' | 'keep' + ignoreCase: boolean + alphabet: string + type: 'custom' +} + interface LineLengthCompareOptions extends BaseCompareOptions { maxLineLength?: number type: 'line-length' } +type SortingFunction = (a: T, b: T) => number + +type IndexByCharacters = Map +let alphabetCache = new Map() + export let compare = ( a: T, b: T, options: CompareOptions, ): number => { - let orderCoefficient = options.order === 'asc' ? 1 : -1 - let sortingFunction: (a: T, b: T) => number + let sortingFunction: SortingFunction let nodeValueGetter = options.nodeValueGetter ?? ((node: T) => node.name) - if (options.type === 'alphabetical') { - let formatString = getFormatStringFunction( - options.ignoreCase, - options.specialCharacters, + + switch (options.type) { + case 'alphabetical': + sortingFunction = getAlphabeticalSortingFunction(options, nodeValueGetter) + break + case 'natural': + sortingFunction = getNaturalSortingFunction(options, nodeValueGetter) + break + case 'custom': + sortingFunction = getCustomSortingFunction(options, nodeValueGetter) + break + case 'line-length': + sortingFunction = getLineLengthSortingFunction(options, nodeValueGetter) + } + + return convertBooleanToSign(options.order === 'asc') * sortingFunction(a, b) +} + +let getAlphabeticalSortingFunction = ( + { specialCharacters, ignoreCase, locales }: AlphabeticalCompareOptions, + nodeValueGetter: (node: T) => string, +): SortingFunction => { + let formatString = getFormatStringFunction(ignoreCase, specialCharacters) + return (aNode: T, bNode: T) => + formatString(nodeValueGetter(aNode)).localeCompare( + formatString(nodeValueGetter(bNode)), + locales, ) - sortingFunction = (aNode, bNode) => - formatString(nodeValueGetter(aNode)).localeCompare( - formatString(nodeValueGetter(bNode)), - options.locales, - ) - } else if (options.type === 'natural') { - let naturalCompare = createNaturalCompare({ - locale: options.locales.toString(), - }) - let formatString = getFormatStringFunction( - options.ignoreCase, - options.specialCharacters, +} + +let getNaturalSortingFunction = ( + { specialCharacters, ignoreCase, locales }: NaturalCompareOptions, + nodeValueGetter: (node: T) => string, +): SortingFunction => { + let naturalCompare = createNaturalCompare({ + locale: locales.toString(), + }) + let formatString = getFormatStringFunction(ignoreCase, specialCharacters) + return (aNode: T, bNode: T) => + naturalCompare( + formatString(nodeValueGetter(aNode)), + formatString(nodeValueGetter(bNode)), ) - sortingFunction = (aNode, bNode) => - naturalCompare( - formatString(nodeValueGetter(aNode)), - formatString(nodeValueGetter(bNode)), - ) - } else { - sortingFunction = (aNode, bNode) => { - let aSize = aNode.size - let bSize = bNode.size - - let { maxLineLength } = options - - if (maxLineLength) { - let isTooLong = (size: number, node: T): undefined | boolean => - size > maxLineLength && node.hasMultipleImportDeclarations - - if (isTooLong(aSize, aNode)) { - aSize = nodeValueGetter(aNode).length + 10 - } - - if (isTooLong(bSize, bNode)) { - bSize = nodeValueGetter(bNode).length + 10 - } - } +} - return aSize - bSize +let getCustomSortingFunction = ( + { specialCharacters, ignoreCase, alphabet }: CustomCompareOptions, + nodeValueGetter: (node: T) => string, +): SortingFunction => { + let formatString = getFormatStringFunction(ignoreCase, specialCharacters) + let indexByCharacters = alphabetCache.get(alphabet) + if (!indexByCharacters) { + indexByCharacters = new Map() + for (let [index, character] of [...alphabet].entries()) { + indexByCharacters.set(character, index) } + alphabetCache.set(alphabet, indexByCharacters) + } + return (aNode: T, bNode: T) => { + let aValue = formatString(nodeValueGetter(aNode)) + let bValue = formatString(nodeValueGetter(bNode)) + // Iterate character by character + // eslint-disable-next-line unicorn/no-for-loop + for (let i = 0; i < aValue.length; i++) { + let aCharacter = aValue[i] + let bCharacter = bValue[i] + let indexOfA = indexByCharacters.get(aCharacter) + let indexOfB = indexByCharacters.get(bCharacter) + indexOfA ??= Infinity + indexOfB ??= Infinity + if (indexOfA !== indexOfB) { + return convertBooleanToSign(indexOfA - indexOfB > 0) + } + } + return 0 } - - return orderCoefficient * sortingFunction(a, b) } +let getLineLengthSortingFunction = + ( + { maxLineLength }: LineLengthCompareOptions, + nodeValueGetter: (node: T) => string, + ): SortingFunction => + (aNode: T, bNode: T) => { + let aSize = aNode.size + let bSize = bNode.size + + if (maxLineLength) { + let isTooLong = (size: number, node: T): undefined | boolean => + size > maxLineLength && node.hasMultipleImportDeclarations + + if (isTooLong(aSize, aNode)) { + aSize = nodeValueGetter(aNode).length + 10 + } + + if (isTooLong(bSize, bNode)) { + bSize = nodeValueGetter(bNode).length + 10 + } + } + + return aSize - bSize + } + let getFormatStringFunction = (ignoreCase: boolean, specialCharacters: 'remove' | 'trim' | 'keep') => (value: string) => { diff --git a/utils/convert-boolean-to-sign.ts b/utils/convert-boolean-to-sign.ts new file mode 100644 index 00000000..dea3c6a2 --- /dev/null +++ b/utils/convert-boolean-to-sign.ts @@ -0,0 +1 @@ +export let convertBooleanToSign = (value: boolean): -1 | 1 => (value ? 1 : -1) diff --git a/rules/get-custom-groups-compare-options.ts b/utils/get-custom-groups-compare-options.ts similarity index 88% rename from rules/get-custom-groups-compare-options.ts rename to utils/get-custom-groups-compare-options.ts index 13ea9040..ad765ca3 100644 --- a/rules/get-custom-groups-compare-options.ts +++ b/utils/get-custom-groups-compare-options.ts @@ -1,19 +1,20 @@ -import type { CompareOptions } from '../utils/compare' +import type { CompareOptions } from './compare' import type { SortingNode } from '../typings' interface Options { customGroups: Record | CustomGroup[] - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable groups: (string[] | string)[] order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string } type CustomGroup = ( | { - type?: 'alphabetical' | 'line-length' | 'natural' + type?: 'alphabetical' | 'line-length' | 'natural' | 'custom' order?: 'desc' | 'asc' } | { @@ -59,6 +60,7 @@ export let getCustomGroupsCompareOptions = ( specialCharacters: options.specialCharacters, type: customGroup?.type ?? options.type, ignoreCase: options.ignoreCase, + alphabet: options.alphabet, locales: options.locales, } } diff --git a/utils/get-settings.ts b/utils/get-settings.ts index ecae141b..49cd0aea 100644 --- a/utils/get-settings.ts +++ b/utils/get-settings.ts @@ -1,7 +1,7 @@ import type { TSESLint } from '@typescript-eslint/utils' export type Settings = Partial<{ - type: 'alphabetical' | 'line-length' | 'natural' + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' partitionByComment: string[] | boolean | string specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable @@ -9,6 +9,7 @@ export type Settings = Partial<{ ignorePattern: string[] order: 'desc' | 'asc' ignoreCase: boolean + alphabet: string }> export let getSettings = ( @@ -25,6 +26,7 @@ export let getSettings = ( 'specialCharacters', 'ignorePattern', 'ignoreCase', + 'alphabet', 'locales', 'order', 'type', diff --git a/utils/validate-custom-sort-configuration.ts b/utils/validate-custom-sort-configuration.ts new file mode 100644 index 00000000..98c82767 --- /dev/null +++ b/utils/validate-custom-sort-configuration.ts @@ -0,0 +1,16 @@ +interface Options { + type: 'alphabetical' | 'line-length' | 'natural' | 'custom' + alphabet: string +} + +export let validateCustomSortConfiguration = ({ + alphabet, + type, +}: Options): void => { + if (type !== 'custom') { + return + } + if (!alphabet.length) { + throw new Error('`alphabet` option must not be empty') + } +} diff --git a/rules/validate-generated-groups-configuration.ts b/utils/validate-generated-groups-configuration.ts similarity index 76% rename from rules/validate-generated-groups-configuration.ts rename to utils/validate-generated-groups-configuration.ts index eba894d9..0b05651a 100644 --- a/rules/validate-generated-groups-configuration.ts +++ b/utils/validate-generated-groups-configuration.ts @@ -1,6 +1,4 @@ -import type { Modifier, Selector } from './sort-classes.types' - -import { validateNoDuplicatedGroups } from '../utils/validate-groups-configuration' +import { validateNoDuplicatedGroups } from './validate-groups-configuration' interface Props { customGroups: Record | BaseCustomGroup[] @@ -50,18 +48,13 @@ let isPredefinedGroup = ( return false } let twoWordsSelector = input.split('-').slice(-2).join('-') - let isTwoWordSelectorValid = allSelectors.includes( - twoWordsSelector as Selector, - ) - if ( - !allSelectors.includes(singleWordSelector as Selector) && - !isTwoWordSelectorValid - ) { + let isTwoWordSelectorValid = allSelectors.includes(twoWordsSelector) + if (!allSelectors.includes(singleWordSelector) && !isTwoWordSelectorValid) { return false } let modifiers = input.split('-').slice(0, isTwoWordSelectorValid ? -2 : -1) return ( new Set(modifiers).size === modifiers.length && - modifiers.every(modifier => allModifiers.includes(modifier as Modifier)) + modifiers.every(modifier => allModifiers.includes(modifier)) ) } diff --git a/vite.config.ts b/vite.config.ts index c41a9340..040661be 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -76,8 +76,11 @@ export default defineConfig({ external: (id: string) => !id.startsWith('.') && !path.isAbsolute(id), }, lib: { + entry: [ + path.resolve(__dirname, 'index.ts'), + path.resolve(__dirname, 'utils', 'alphabet.ts'), + ], fileName: (_format, entryName) => `${entryName}.js`, - entry: path.resolve(__dirname, 'index.ts'), name: 'eslint-plugin-perfectionist', formats: ['cjs'], },