From ac7d709271982f87a0eb9fc460c3e1dbe228bf49 Mon Sep 17 00:00:00 2001 From: Hugo <60015232+hugop95@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:23:35 +0100 Subject: [PATCH] feat: add custom sort type through custom alphabet * feat: adds alphabet generator * We will be using that class in tests as well. * feat: adds `custom` sort type * feat: adds custom sort validation * refactor: create function for each sort type * test: adds tests for all rules * The custom sort generated mimics `localeCompare()` for simplicity. * I have added a single test with the `custom` sort type for each rule. I don't think that adding as many tests as other types is necessary: the objective is not to test that the behavior of a custom sort works for all rules, but rather to check that the rule handles that sorting type. * Custom sorting behavior should be verified in `compare.ts`. * refactor: moves some util files to `utils` folder * docs: updates main pages * docs: updates rules * docs: improves style * docs: fixes invalid documentation * fix: fix invalid config creation * fix: fixes typings * refactor: [FEEDBACK] avoids using undefined Co-authored-by: Azat S. * refactor: [FEEDBACK] avoids using undefined Co-authored-by: Azat S. * refactor: [FEEDBACK] reduces comments length * docs: [FEEDBACK] fixes missing `Alphabet` * docs: [FEEDBACK] adds Astro section * feat: [FEEDBACK] adds `convertBooleanToSign` function * docs: added `Alphabet` documentation - Removed `removeUnicodePlane` function, which is likely not that useful for users. * build: [FEEDBACK] adds `eslint-plugin-perfectionist/alphabet` export * docs: [FEEDBACK] adds `eslint-plugin-perfectionist/alphabet` export * style: ESLint fix --------- Co-authored-by: Azat S. --- docs/content/configs/recommended-custom.mdx | 199 ++++++++ docs/content/guide/getting-started.mdx | 3 +- docs/content/rules/sort-array-includes.mdx | 11 + docs/content/rules/sort-classes.mdx | 11 + docs/content/rules/sort-decorators.mdx | 11 + docs/content/rules/sort-enums.mdx | 11 + docs/content/rules/sort-exports.mdx | 11 + docs/content/rules/sort-heritage-clauses.mdx | 11 + docs/content/rules/sort-imports.mdx | 11 + docs/content/rules/sort-interfaces.mdx | 27 +- .../content/rules/sort-intersection-types.mdx | 11 + docs/content/rules/sort-jsx-props.mdx | 11 + docs/content/rules/sort-maps.mdx | 11 + docs/content/rules/sort-modules.mdx | 11 + docs/content/rules/sort-named-exports.mdx | 11 + docs/content/rules/sort-named-imports.mdx | 11 + docs/content/rules/sort-object-types.mdx | 25 +- docs/content/rules/sort-objects.mdx | 11 + docs/content/rules/sort-sets.mdx | 11 + docs/content/rules/sort-switch-case.mdx | 11 + docs/content/rules/sort-union-types.mdx | 11 + .../rules/sort-variable-declarations.mdx | 11 + docs/pages/configs/index.astro | 5 + docs/utils/pages.ts | 4 + index.ts | 12 +- package.json | 4 + readme.md | 7 +- rules/sort-array-includes.ts | 6 +- rules/sort-classes.ts | 10 +- rules/sort-classes.types.ts | 3 +- rules/sort-decorators.ts | 9 +- rules/sort-enums.ts | 10 +- rules/sort-exports.ts | 8 +- rules/sort-heritage-clauses.ts | 9 +- rules/sort-imports.ts | 9 +- rules/sort-interfaces.ts | 1 + rules/sort-intersection-types.ts | 9 +- rules/sort-jsx-props.ts | 8 +- rules/sort-maps.ts | 9 +- rules/sort-modules.ts | 10 +- rules/sort-modules.types.ts | 3 +- rules/sort-named-exports.ts | 9 +- rules/sort-named-imports.ts | 9 +- rules/sort-object-types.ts | 9 +- rules/sort-object-types.types.ts | 3 +- rules/sort-objects.ts | 8 +- rules/sort-switch-case.ts | 9 +- rules/sort-union-types.ts | 9 +- rules/sort-variable-declarations.ts | 9 +- test/alphabet.test.ts | 276 +++++++++++ test/compare.test.ts | 119 +++++ test/get-settings.test.ts | 1 + test/sort-array-includes.test.ts | 60 +++ test/sort-classes.test.ts | 168 +++++++ test/sort-decorators.test.ts | 113 +++++ test/sort-enums.test.ts | 60 +++ test/sort-exports.test.ts | 61 +++ test/sort-heritage-clauses.test.ts | 91 ++++ test/sort-imports.test.ts | 48 ++ test/sort-interfaces.test.ts | 65 +++ test/sort-intersection-types.test.ts | 45 ++ test/sort-jsx-props.test.ts | 69 +++ test/sort-maps.test.ts | 54 +++ test/sort-modules.test.ts | 145 ++++++ test/sort-named-exports.test.ts | 55 +++ test/sort-named-imports.test.ts | 45 ++ test/sort-object-types.test.ts | 57 +++ test/sort-objects.test.ts | 64 +++ test/sort-sets.test.ts | 60 +++ test/sort-switch-case.test.ts | 109 +++++ test/sort-union-types.test.ts | 45 ++ test/sort-variable-declarations.test.ts | 42 ++ ...validate-custom-sort-configuration.test.ts | 25 + ...ate-generated-groups-configuration.test.ts | 2 +- utils/alphabet.ts | 434 ++++++++++++++++++ utils/common-json-schemas.ts | 7 +- utils/compare.ts | 158 +++++-- utils/convert-boolean-to-sign.ts | 1 + .../get-custom-groups-compare-options.ts | 8 +- utils/get-settings.ts | 4 +- utils/validate-custom-sort-configuration.ts | 16 + ...validate-generated-groups-configuration.ts | 15 +- vite.config.ts | 5 +- 83 files changed, 3068 insertions(+), 112 deletions(-) create mode 100644 docs/content/configs/recommended-custom.mdx create mode 100644 test/alphabet.test.ts create mode 100644 test/validate-custom-sort-configuration.test.ts create mode 100644 utils/alphabet.ts create mode 100644 utils/convert-boolean-to-sign.ts rename {rules => utils}/get-custom-groups-compare-options.ts (88%) create mode 100644 utils/validate-custom-sort-configuration.ts rename {rules => utils}/validate-generated-groups-configuration.ts (76%) diff --git a/docs/content/configs/recommended-custom.mdx b/docs/content/configs/recommended-custom.mdx new file mode 100644 index 000000000..0214801c7 --- /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 c10fbe0b5..3f8e528bb 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 48434a318..c0a3f1679 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 5065aae82..e3da41d6e 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 f8519e576..62e316351 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 d8b58833e..de6cd8586 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 12b195374..6968ade5c 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 5a0ab529f..2ef7e777b 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 057a4588c..320f1e1b8 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 b62f3cbb0..2778809bb 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 90f5b333a..b56f6d784 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 3b030dcf2..4fd7b6a7d 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 febb1e5e1..acc41f3cc 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 2a57d71e6..1db7aa7c7 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 6ba10b2de..e96fddef5 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 d7fb3a3f3..fddb8fd5c 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 afce15858..6502ba459 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 b509d2f3a..2a8248460 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 b20e93b51..99bc8d876 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 455618b0f..070f549d1 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 162f44106..f331603cc 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 ed2bc97e5..c930f91d8 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 ff6ec5c47..9e545b63a 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 5d1b85168..a922d734f 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 c69cf4a29..0d47ebbee 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 e46a6de31..facb2d785 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 63dc618c6..713e42ec6 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 eb30651f4..b7dbb5751 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 31d150619..6fe28edf0 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 754865c5f..adde1916f 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 a87a96067..26e5cea3f 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 7699aae41..8966ed305 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 f4ccee2a4..04624aaf1 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 17370001a..b3a5c30be 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 9c9f889b2..21bf981d0 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 5eb6bf105..6267fcf9f 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 130fc337e..fe14c2f94 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 157b4b96e..b667072f0 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 5d24a02cb..84532115c 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 31501abf8..d1c511314 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 20b337094..838c2ef55 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 8aafe7cb7..de586374a 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 a259d3b3f..ec17f4fd2 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 95c6647af..cbd5d6903 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 443af3b4d..8ce16ddb6 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 c79643e11..82ea83306 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 edc1a7c6d..bf41408bd 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 e8b45874f..8fa1601d3 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 25e477700..a1d04d42e 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 000000000..ecc1846e2 --- /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 245118aa7..e2c9445fa 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 482c7a7be..98a7a6530 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 b9bb5cd82..0959acb1c 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 a07313589..f46766e94 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 b96d506b6..2d66c1793 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 fd8628e5d..ca7ffc1fb 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 9425f1731..004600b35 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 bd3e3d09a..32659ca1e 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 e624f35cd..e2f672ab2 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 ec37d507a..3c7952078 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 cc61cf082..d1cadbb6b 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 d84e960ba..675d994be 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 b0916d4c8..5d2ef7515 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 1517a8624..caaf58045 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 778d576e2..a80e60946 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 55c81d612..a54c28123 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 6fb5b8bf0..8c8709f0f 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 b82eff75a..ca7c7ee68 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 ab3480ae2..460f26e3e 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 a5657ba92..61d6343e8 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 3f7e0631a..49129ade9 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 9fcbbd45f..686726d5d 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 000000000..aa2587c40 --- /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 11a2284d9..e4d38c2a4 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 000000000..f53b23820 --- /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 29564ece4..f3698bbc0 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 38dcf0cf1..13aba43c1 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 000000000..dea3c6a23 --- /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 13ea9040d..ad765ca34 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 ecae141be..49cd0aea1 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 000000000..98c827671 --- /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 eba894d95..0b05651a5 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 c41a93407..040661be8 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'], },