Repository: azat-io/eslint-plugin-perfectionist
Pull request #447 (2025-01-23 12:01:25)

feat(sort-enums): adds `groups`, `customGroups` and `newlinesBetween`
### Description

This PR adds the following options to `sort-enums`:
- `groups` (no predefined group other than `unknown`)
- `customGroups`:
  - `elementNamePattern`
  - `elementValuePattern`
- `newlinesBetween`

### What is the purpose of this pull request?

- [x] New Feature



Patch:
diff --git a/docs/content/rules/sort-enums.mdx b/docs/content/rules/sort-enums.mdx
index 06d8fd0..0123bdc 100644
--- a/docs/content/rules/sort-enums.mdx
+++ b/docs/content/rules/sort-enums.mdx
@@ -197,6 +197,110 @@ enum Enum {
 
 Each group of enum members (separated by empty lines) is treated independently, and the order within each group is preserved.
 
+### newlinesBetween
+
+<sub>default: `'ignore'`</sub>
+
+Specifies how new lines should be handled between enum members.
+
+- `ignore` — Do not report errors related to new lines between enum members.
+- `always` — Enforce one new line between each group, and forbid new lines inside a group.
+- `never` — No new lines are allowed in enums.
+
+You can also enforce the newline behavior between two specific groups through the `groups` options.
+
+See the [`groups`](#newlines-between-groups) option.
+
+This option is only applicable when `partitionByNewLine` is `false`.
+
+### groups
+
+<sub>
+  type: `Array<string | string[]>`
+</sub>
+<sub>default: `[]`</sub>
+
+Allows you to specify a list of groups for sorting. Groups help organize elements into categories.
+
+Each element will be assigned a single group specified in the `groups` option (or the `unknown` group if no match is found).
+The order of items in the `groups` option determines how groups are ordered.
+
+Within a given group, members will be sorted according to the `type`, `order`, `ignoreCase`, etc. options.
+
+Individual groups can be combined together by placing them in an array. The order of groups in that array does not matter.
+All members of the groups in the array will be sorted together as if they were part of a single group.
+
+#### Newlines between groups
+
+You may place `newlinesBetween` objects between your groups to enforce the newline behavior between two specific groups.
+
+See the [`newlinesBetween`](#newlinesbetween) option.
+
+This feature is only applicable when `partitionByNewLine` is false.
+
+```ts
+{
+  newlinesBetween: 'always',
+  groups: [
+    'a',
+    { newlinesBetween: 'never' }, // Overrides the global newlinesBetween option
+    'b',
+  ]
+}
+```
+
+### customGroups
+
+<sub>
+  type: `Array<CustomGroupDefinition | CustomGroupAnyOfDefinition>`
+</sub>
+<sub>default: `{}`</sub>
+
+You can define your own groups and use regexp patterns to match specific elements.
+
+A custom group definition may follow one of the two following interfaces:
+
+```ts
+interface CustomGroupDefinition {
+  groupName: string
+  type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
+  order?: 'asc' | 'desc'
+  elementNamePattern?: string
+}
+
+```
+An enum member will match a `CustomGroupDefinition` group if it matches all the filters of the custom group's definition.
+
+or:
+
+```ts
+interface CustomGroupAnyOfDefinition {
+  groupName: string
+  type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
+  order?: 'asc' | 'desc'
+  anyOf: Array<{
+      elementNamePattern?: string
+  }>
+}
+```
+
+An enum member will match a `CustomGroupAnyOfDefinition` group if it matches all the filters of at least one of the `anyOf` items.
+
+#### Attributes
+
+- `groupName`: The group's name, which needs to be put in the `groups` option.
+- `elementNamePattern`: If entered, will check that the name of the element matches the pattern entered.
+- `elementValuePattern`: If entered, will check that the value of the element matches the pattern entered.
+- `type`: Overrides the sort type for that custom group. `unsorted` will not sort the group.
+- `order`: Overrides the sort order for that custom group
+
+#### Match importance
+
+The `customGroups` list is ordered:
+The first custom group definition that matches an element will be used.
+
+Custom groups have a higher priority than any predefined group.
+
 ## Usage
 
 <CodeTabs
@@ -221,8 +325,11 @@ Each group of enum members (separated by empty lines) is treated independently,
                   specialCharacters: 'keep',
                   partitionByComment: false,
                   partitionByNewLine: false,
+                  newlinesBetween: 'ignore',
                   sortByValue: false,
-                  forceNumericSort: false
+                  forceNumericSort: false,
+                  groups: [],
+                  customGroups: [],
                 },
               ],
             },
@@ -249,8 +356,11 @@ Each group of enum members (separated by empty lines) is treated independently,
                 specialCharacters: 'keep',
                 partitionByComment: false,
                 partitionByNewLine: false,
+                newlinesBetween: 'ignore',
                 sortByValue: false,
                 forceNumericSort: false,
+                groups: [],
+                customGroups: [],
               },
             ],
           },
diff --git a/docs/content/rules/sort-maps.mdx b/docs/content/rules/sort-maps.mdx
index a4f7546..ff77438 100644
--- a/docs/content/rules/sort-maps.mdx
+++ b/docs/content/rules/sort-maps.mdx
@@ -188,6 +188,10 @@ Specifies how new lines should be handled between map members.
 - `always` — Enforce one new line between each group, and forbid new lines inside a group.
 - `never` — No new lines are allowed in maps.
 
+You can also enforce the newline behavior between two specific groups through the `groups` options.
+
+See the [`groups`](#newlines-between-groups) option.
+
 This option is only applicable when `partitionByNewLine` is `false`.
 
 ### useConfigurationIf
@@ -252,6 +256,25 @@ Within a given group, members will be sorted according to the `type`, `order`, `
 Individual groups can be combined together by placing them in an array. The order of groups in that array does not matter.
 All members of the groups in the array will be sorted together as if they were part of a single group.
 
+#### Newlines between groups
+
+You may place `newlinesBetween` objects between your groups to enforce the newline behavior between two specific groups.
+
+See the [`newlinesBetween`](#newlinesbetween) option.
+
+This feature is only applicable when `partitionByNewLine` is false.
+
+```ts
+{
+  newlinesBetween: 'always',
+  groups: [
+    'a',
+    { newlinesBetween: 'never' }, // Overrides the global newlinesBetween option
+    'b',
+  ]
+}
+```
+
 ### customGroups
 
 <sub>
@@ -272,7 +295,7 @@ interface CustomGroupDefinition {
 }
 
 ```
-An array element will match a `CustomGroupDefinition` group if it matches all the filters of the custom group's definition.
+A map element will match a `CustomGroupDefinition` group if it matches all the filters of the custom group's definition.
 
 or:
 
@@ -287,7 +310,7 @@ interface CustomGroupAnyOfDefinition {
 }
 ```
 
-An array element will match a `CustomGroupAnyOfDefinition` group if it matches all the filters of at least one of the `anyOf` items.
+A map element will match a `CustomGroupAnyOfDefinition` group if it matches all the filters of at least one of the `anyOf` items.
 
 #### Attributes
 
diff --git a/docs/content/rules/sort-objects.mdx b/docs/content/rules/sort-objects.mdx
index 3514091..c14cfd8 100644
--- a/docs/content/rules/sort-objects.mdx
+++ b/docs/content/rules/sort-objects.mdx
@@ -258,6 +258,10 @@ Specifies how new lines should be handled between object groups.
 - `always` — Enforce one new line between each group, and forbid new lines inside a group.
 - `never` — No new lines are allowed in objects.
 
+You can also enforce the newline behavior between two specific groups through the `groups` options.
+
+See the [`groups`](#newlines-between-groups) option.
+
 This option is only applicable when `partitionByNewLine` is `false`.
 
 ### styledComponents
@@ -446,6 +450,25 @@ interface Test {
 - `member`.
 - `unknown`.
 
+#### Newlines between groups
+
+You may place `newlinesBetween` objects between your groups to enforce the newline behavior between two specific groups.
+
+See the [`newlinesBetween`](#newlinesbetween) option.
+
+This feature is only applicable when `partitionByNewLine` is false.
+
+```ts
+{
+  newlinesBetween: 'always',
+  groups: [
+    'a',
+    { newlinesBetween: 'never' }, // Overrides the global newlinesBetween option
+    'b',
+  ]
+}
+```
+
 ### customGroups
 
 <Important title="Migrating from the old API">
diff --git a/rules/sort-enums.ts b/rules/sort-enums.ts
index 82435c7..d93a1d3 100644
--- a/rules/sort-enums.ts
+++ b/rules/sort-enums.ts
@@ -1,22 +1,29 @@
 import type { TSESTree } from '@typescript-eslint/types'
 
-import type {
-  PartitionByCommentOption,
-  CommonOptions,
-} from '../types/common-options'
 import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies'
 import type { CompareOptions } from '../utils/compare'
+import type { Options } from './sort-enums/types'
 
 import {
+  buildCustomGroupsArrayJsonSchema,
   partitionByCommentJsonSchema,
   partitionByNewLineJsonSchema,
+  newlinesBetweenJsonSchema,
+  customGroupsJsonSchema,
   buildTypeJsonSchema,
   commonJsonSchemas,
+  groupsJsonSchema,
 } from '../utils/common-json-schemas'
+import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
+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 { sortNodesByDependencies } from '../utils/sort-nodes-by-dependencies'
 import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
+import { doesCustomGroupMatch } from './sort-enums/does-custom-group-match'
 import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
+import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
+import { singleCustomGroupJsonSchema } from './sort-enums/types'
 import { createEslintRule } from '../utils/create-eslint-rule'
 import { reportAllErrors } from '../utils/report-all-errors'
 import { shouldPartition } from '../utils/should-partition'
@@ -25,39 +32,35 @@ import { getSourceCode } from '../utils/get-source-code'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { sortNodes } from '../utils/sort-nodes'
+import { useGroups } from '../utils/use-groups'
 import { complete } from '../utils/complete'
 
-export type Options = [
-  Partial<
-    {
-      type: 'alphabetical' | 'line-length' | 'natural' | 'custom'
-      partitionByComment: PartitionByCommentOption
-      partitionByNewLine: boolean
-      forceNumericSort: boolean
-      sortByValue: boolean
-    } & CommonOptions
-  >,
-]
+type MESSAGE_ID =
+  | 'missedSpacingBetweenEnumsMembers'
+  | 'extraSpacingBetweenEnumsMembers'
+  | 'unexpectedEnumsDependencyOrder'
+  | 'unexpectedEnumsGroupOrder'
+  | 'unexpectedEnumsOrder'
 
 interface SortEnumsSortingNode
   extends SortingNodeWithDependencies<TSESTree.TSEnumMember> {
   numericValue: number | null
 }
 
-type MESSAGE_ID = 'unexpectedEnumsDependencyOrder' | 'unexpectedEnumsOrder'
-
 let defaultOptions: Required<Options[0]> = {
   partitionByComment: false,
   partitionByNewLine: false,
   specialCharacters: 'keep',
+  newlinesBetween: 'ignore',
   forceNumericSort: false,
   type: 'alphabetical',
   sortByValue: false,
   ignoreCase: true,
   locales: 'en-US',
+  customGroups: [],
   alphabet: '',
   order: 'asc',
+  groups: [],
 }
 
 export default createEslintRule<Options, MESSAGE_ID>({
@@ -74,6 +77,13 @@ export default createEslintRule<Options, MESSAGE_ID>({
       let settings = getSettings(context.settings)
       let options = complete(context.options.at(0), settings, defaultOptions)
       validateCustomSortConfiguration(options)
+      validateGeneratedGroupsConfiguration({
+        customGroups: options.customGroups,
+        groups: options.groups,
+        selectors: [],
+        modifiers: [],
+      })
+      validateNewlinesAndPartitionConfiguration(options)
 
       let sourceCode = getSourceCode(context)
       let eslintDisabledLines = getEslintDisabledLines({
@@ -124,6 +134,32 @@ export default createEslintRule<Options, MESSAGE_ID>({
               enumDeclaration.id.name,
             )
           }
+
+          let name =
+            member.id.type === 'Literal'
+              ? `${member.id.value}`
+              : sourceCode.getText(member.id)
+
+          let { defineGroup, getGroup } = useGroups(options)
+          for (let customGroup of options.customGroups) {
+            if (
+              doesCustomGroupMatch({
+                elementValue: sourceCode.getText(member.initializer),
+                elementName: name,
+                customGroup,
+              })
+            ) {
+              defineGroup(customGroup.groupName, true)
+              /**
+               * If the custom group is not referenced in the `groups` option, it
+               * will be ignored
+               */
+              if (getGroup() === customGroup.groupName) {
+                break
+              }
+            }
+          }
+
           let lastSortingNode = accumulator.at(-1)?.at(-1)
           let sortingNode: SortEnumsSortingNode = {
             numericValue: member.initializer
@@ -131,14 +167,12 @@ export default createEslintRule<Options, MESSAGE_ID>({
                   member.initializer,
                 ) /* v8 ignore next - Unsure how we can reach that case */
               : null,
-            name:
-              member.id.type === 'Literal'
-                ? `${member.id.value}`
-                : sourceCode.getText(member.id),
             isEslintDisabled: isNodeEslintDisabled(member, eslintDisabledLines),
             size: rangeToDiff(member, sourceCode),
+            group: getGroup(),
             node: member,
             dependencies,
+            name,
           }
 
           if (
@@ -166,7 +200,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
           !Number.isNaN(sortingNode.numericValue),
       )
 
-      let compareOptions: CompareOptions<SortEnumsSortingNode> = {
+      let compareOptions: CompareOptions<SortEnumsSortingNode> &
+        Required<Options[0]> = {
+        ...options,
         // Get the enum value rather than the name if needed.
         nodeValueGetter:
           options.sortByValue || (isNumericEnum && options.forceNumericSort)
@@ -188,11 +224,6 @@ export default createEslintRule<Options, MESSAGE_ID>({
           isNumericEnum && (options.forceNumericSort || options.sortByValue)
             ? 'natural'
             : options.type,
-        specialCharacters: options.specialCharacters,
-        ignoreCase: options.ignoreCase,
-        alphabet: options.alphabet,
-        locales: options.locales,
-        order: options.order,
       }
 
       let sortNodesExcludingEslintDisabled = (
@@ -200,7 +231,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
       ): SortEnumsSortingNode[] =>
         sortNodesByDependencies(
           formattedMembers.flatMap(sortingNodes =>
-            sortNodes(sortingNodes, compareOptions, {
+            sortNodesByGroups(sortingNodes, compareOptions, {
+              getGroupCompareOptions: groupNumber =>
+                getCustomGroupsCompareOptions(compareOptions, groupNumber),
               ignoreEslintDisabledNodes,
             }),
           ),
@@ -211,7 +244,10 @@ export default createEslintRule<Options, MESSAGE_ID>({
 
       reportAllErrors<MESSAGE_ID>({
         availableMessageIds: {
+          missedSpacingBetweenMembers: 'missedSpacingBetweenEnumsMembers',
+          extraSpacingBetweenMembers: 'extraSpacingBetweenEnumsMembers',
           unexpectedDependencyOrder: 'unexpectedEnumsDependencyOrder',
+          unexpectedGroupOrder: 'unexpectedEnumsGroupOrder',
           unexpectedOrder: 'unexpectedEnumsOrder',
         },
         sortNodesExcludingEslintDisabled,
@@ -232,21 +268,35 @@ export default createEslintRule<Options, MESSAGE_ID>({
               'Will always sort numeric enums by their value regardless of the sort type specified.',
             type: 'boolean',
           },
+          customGroups: {
+            oneOf: [
+              customGroupsJsonSchema,
+              buildCustomGroupsArrayJsonSchema({ singleCustomGroupJsonSchema }),
+            ],
+          },
           sortByValue: {
             description: 'Compare enum values instead of names.',
             type: 'boolean',
           },
           partitionByComment: partitionByCommentJsonSchema,
           partitionByNewLine: partitionByNewLineJsonSchema,
+          newlinesBetween: newlinesBetweenJsonSchema,
           type: buildTypeJsonSchema(),
+          groups: groupsJsonSchema,
         },
         additionalProperties: false,
         type: 'object',
       },
     ],
     messages: {
+      unexpectedEnumsGroupOrder:
+        'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
       unexpectedEnumsDependencyOrder:
         'Expected dependency "{{right}}" to come before "{{nodeDependentOnRight}}".',
+      missedSpacingBetweenEnumsMembers:
+        'Missed spacing between "{{left}}" and "{{right}}" members.',
+      extraSpacingBetweenEnumsMembers:
+        'Extra spacing between "{{left}}" and "{{right}}" members.',
       unexpectedEnumsOrder: 'Expected "{{right}}" to come before "{{left}}".',
     },
     docs: {
diff --git a/rules/sort-enums/does-custom-group-match.ts b/rules/sort-enums/does-custom-group-match.ts
new file mode 100644
index 0000000..a5392c3
--- /dev/null
+++ b/rules/sort-enums/does-custom-group-match.ts
@@ -0,0 +1,48 @@
+import type { SingleCustomGroup, AnyOfCustomGroup } from './types'
+
+import { matches } from '../../utils/matches'
+
+interface DoesCustomGroupMatchProps {
+  customGroup: SingleCustomGroup | AnyOfCustomGroup
+  elementValue: string
+  elementName: string
+}
+
+export let doesCustomGroupMatch = (
+  props: DoesCustomGroupMatchProps,
+): boolean => {
+  if ('anyOf' in props.customGroup) {
+    // At least one subgroup must match
+    return props.customGroup.anyOf.some(subgroup =>
+      doesCustomGroupMatch({ ...props, customGroup: subgroup }),
+    )
+  }
+
+  if (
+    'elementNamePattern' in props.customGroup &&
+    props.customGroup.elementNamePattern
+  ) {
+    let matchesElementNamePattern: boolean = matches(
+      props.elementName,
+      props.customGroup.elementNamePattern,
+    )
+    if (!matchesElementNamePattern) {
+      return false
+    }
+  }
+
+  if (
+    'elementValuePattern' in props.customGroup &&
+    props.customGroup.elementValuePattern
+  ) {
+    let matchesElementValuePattern: boolean = matches(
+      props.elementValue,
+      props.customGroup.elementValuePattern,
+    )
+    if (!matchesElementValuePattern) {
+      return false
+    }
+  }
+
+  return true
+}
diff --git a/rules/sort-enums/types.ts b/rules/sort-enums/types.ts
new file mode 100644
index 0000000..e083605
--- /dev/null
+++ b/rules/sort-enums/types.ts
@@ -0,0 +1,54 @@
+import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'
+
+import type {
+  PartitionByCommentOption,
+  CommonOptions,
+  GroupsOptions,
+} from '../../types/common-options'
+
+import {
+  elementValuePatternJsonSchema,
+  elementNamePatternJsonSchema,
+} from '../../utils/common-json-schemas'
+
+export type Options = Partial<
+  {
+    type: 'alphabetical' | 'line-length' | 'natural' | 'custom'
+    newlinesBetween: 'ignore' | 'always' | 'never'
+    partitionByComment: PartitionByCommentOption
+    groups: GroupsOptions<Group>
+    partitionByNewLine: boolean
+    customGroups: CustomGroup[]
+    forceNumericSort: boolean
+    sortByValue: boolean
+  } & CommonOptions
+>[]
+
+export interface SingleCustomGroup {
+  elementValuePattern?: string
+  elementNamePattern?: string
+}
+
+export interface AnyOfCustomGroup {
+  anyOf: SingleCustomGroup[]
+}
+
+type CustomGroup = (
+  | {
+      order?: Options[0]['order']
+      type?: Options[0]['type']
+    }
+  | {
+      type?: 'unsorted'
+    }
+) &
+  (SingleCustomGroup | AnyOfCustomGroup) & {
+    groupName: string
+  }
+
+type Group = 'unknown' | string
+
+export let singleCustomGroupJsonSchema: Record<string, JSONSchema4> = {
+  elementValuePattern: elementValuePatternJsonSchema,
+  elementNamePattern: elementNamePatternJsonSchema,
+}
diff --git a/utils/get-custom-groups-compare-options.ts b/utils/get-custom-groups-compare-options.ts
index ffd4036..cc4d996 100644
--- a/utils/get-custom-groups-compare-options.ts
+++ b/utils/get-custom-groups-compare-options.ts
@@ -54,14 +54,11 @@ export let getCustomGroupsCompareOptions = <T extends SortingNode>(
     return null
   }
   return {
+    ...options,
     order:
       customGroup && 'order' in customGroup && customGroup.order
         ? customGroup.order
         : options.order,
-    specialCharacters: options.specialCharacters,
     type: customGroup?.type ?? options.type,
-    ignoreCase: options.ignoreCase,
-    alphabet: options.alphabet,
-    locales: options.locales,
   }
 }


Test patch:
diff --git a/test/rules/sort-enums.test.ts b/test/rules/sort-enums.test.ts
index 6a81b51..3e310ae 100644
--- a/test/rules/sort-enums.test.ts
+++ b/test/rules/sort-enums.test.ts
@@ -2,7 +2,7 @@ import { RuleTester } from '@typescript-eslint/rule-tester'
 import { afterAll, describe, it } from 'vitest'
 import dedent from 'dedent'
 
-import type { Options } from '../../rules/sort-enums'
+import type { Options } from '../../rules/sort-enums/types'
 
 import { Alphabet } from '../../utils/alphabet'
 import rule from '../../rules/sort-enums'
@@ -905,6 +905,609 @@ describe(ruleName, () => {
         valid: [],
       },
     )
+
+    describe(`${ruleName}: custom groups`, () => {
+      ruleTester.run(`${ruleName}: filters on elementNamePattern`, rule, {
+        invalid: [
+          {
+            errors: [
+              {
+                data: {
+                  rightGroup: 'keysStartingWithHello',
+                  leftGroup: 'unknown',
+                  right: 'HELLO_KEY',
+                  left: 'B',
+                },
+                messageId: 'unexpectedEnumsGroupOrder',
+              },
+            ],
+            options: [
+              {
+                customGroups: [
+                  {
+                    groupName: 'keysStartingWithHello',
+                    elementNamePattern: 'HELLO*',
+                  },
+                ],
+                groups: ['keysStartingWithHello', 'unknown'],
+              },
+            ],
+            output: dedent`
+              enum Enum {
+                HELLO_KEY = 3,
+                A = 1,
+                B = 2,
+              }
+            `,
+            code: dedent`
+              enum Enum {
+                A = 1,
+                B = 2,
+                HELLO_KEY = 3,
+              }
+            `,
+          },
+        ],
+        valid: [],
+      })
+
+      ruleTester.run(`${ruleName}: filters on elementValuePattern`, rule, {
+        invalid: [
+          {
+            options: [
+              {
+                customGroups: [
+                  {
+                    groupName: 'valuesStartingWithHello',
+                    elementValuePattern: 'HELLO*',
+                  },
+                ],
+                groups: ['valuesStartingWithHello', 'unknown'],
+              },
+            ],
+            errors: [
+              {
+                data: {
+                  rightGroup: 'valuesStartingWithHello',
+                  leftGroup: 'unknown',
+                  right: 'Z',
+                  left: 'B',
+                },
+                messageId: 'unexpectedEnumsGroupOrder',
+              },
+            ],
+            output: dedent`
+              enum Enum {
+                Z = 'HELLO_KEY',
+                A = 'A',
+                B = 'B',
+              }
+            `,
+            code: dedent`
+              enum Enum {
+                A = 'A',
+                B = 'B',
+                Z = 'HELLO_KEY',
+              }
+            `,
+          },
+        ],
+        valid: [],
+      })
+
+      ruleTester.run(
+        `${ruleName}: sort custom groups by overriding 'type' and 'order'`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: '_BB',
+                    left: '_A',
+                  },
+                  messageId: 'unexpectedEnumsOrder',
+                },
+                {
+                  data: {
+                    right: '_CCC',
+                    left: '_BB',
+                  },
+                  messageId: 'unexpectedEnumsOrder',
+                },
+                {
+                  data: {
+                    right: '_DDDD',
+                    left: '_CCC',
+                  },
+                  messageId: 'unexpectedEnumsOrder',
+                },
+                {
+                  data: {
+                    rightGroup: 'reversedStartingWith_ByLineLength',
+                    leftGroup: 'unknown',
+                    right: '_EEE',
+                    left: 'M',
+                  },
+                  messageId: 'unexpectedEnumsGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  customGroups: [
+                    {
+                      groupName: 'reversedStartingWith_ByLineLength',
+                      elementNamePattern: '_',
+                      type: 'line-length',
+                      order: 'desc',
+                    },
+                  ],
+                  groups: ['reversedStartingWith_ByLineLength', 'unknown'],
+                  type: 'alphabetical',
+                  order: 'asc',
+                },
+              ],
+              output: dedent`
+                enum Enum {
+                  _DDDD = null,
+                  _CCC = null,
+                  _EEE = null,
+                  _BB = null,
+                  _FF = null,
+                  _A = null,
+                  _G = null,
+                  M = null,
+                  O = null,
+                  P = null,
+                }
+              `,
+              code: dedent`
+                enum Enum {
+                  _A = null,
+                  _BB = null,
+                  _CCC = null,
+                  _DDDD = null,
+                  M = null,
+                  _EEE = null,
+                  _FF = null,
+                  _G = null,
+                  O = null,
+                  P = null,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}: does not sort custom groups with 'unsorted' type`,
+        rule,
+        {
+          invalid: [
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      groupName: 'unsortedStartingWith_',
+                      elementNamePattern: '_',
+                      type: 'unsorted',
+                    },
+                  ],
+                  groups: ['unsortedStartingWith_', 'unknown'],
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'unsortedStartingWith_',
+                    leftGroup: 'unknown',
+                    right: '_C',
+                    left: 'M',
+                  },
+                  messageId: 'unexpectedEnumsGroupOrder',
+                },
+              ],
+              output: dedent`
+                enum Enum {
+                  _B = null,
+                  _A = null,
+                  _D = null,
+                  _E = null,
+                  _C = null,
+                  M = null,
+                }
+              `,
+              code: dedent`
+                enum Enum {
+                  _B = null,
+                  _A = null,
+                  _D = null,
+                  _E = null,
+                  M = null,
+                  _C = null,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(`${ruleName}: sort custom group blocks`, rule, {
+        invalid: [
+          {
+            options: [
+              {
+                customGroups: [
+                  {
+                    anyOf: [
+                      {
+                        elementNamePattern: 'FOO',
+                      },
+                      {
+                        elementNamePattern: 'Foo',
+                      },
+                    ],
+                    groupName: 'elementsIncludingFoo',
+                  },
+                ],
+                groups: ['elementsIncludingFoo', 'unknown'],
+              },
+            ],
+            errors: [
+              {
+                data: {
+                  rightGroup: 'elementsIncludingFoo',
+                  leftGroup: 'unknown',
+                  right: 'C_FOO',
+                  left: 'A',
+                },
+                messageId: 'unexpectedEnumsGroupOrder',
+              },
+            ],
+            output: dedent`
+              enum Enum {
+                C_FOO = null,
+                FOO = null,
+                A = null,
+              }
+            `,
+            code: dedent`
+              enum Enum {
+                A = null,
+                C_FOO = null,
+                FOO = null,
+              }
+            `,
+          },
+        ],
+        valid: [],
+      })
+
+      ruleTester.run(
+        `${ruleName}: allows to use regex for element names in custom groups`,
+        rule,
+        {
+          valid: [
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      elementNamePattern: '^(?!.*FOO).*$',
+                      groupName: 'elementsWithoutFoo',
+                    },
+                  ],
+                  groups: ['unknown', 'elementsWithoutFoo'],
+                  type: 'alphabetical',
+                },
+              ],
+              code: dedent`
+                enum Enum {
+                  I_HAVE_FOO_IN_MY_NAME = null,
+                  ME_TOO_I_HAVE_FOO = null,
+                  A = null,
+                  B = null,
+                }
+              `,
+            },
+          ],
+          invalid: [],
+        },
+      )
+    })
+
+    describe(`${ruleName}: newlinesBetween`, () => {
+      ruleTester.run(
+        `${ruleName}(${type}): removes newlines when never`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'Y',
+                    left: 'A',
+                  },
+                  messageId: 'extraSpacingBetweenEnumsMembers',
+                },
+                {
+                  data: {
+                    right: 'B',
+                    left: 'Z',
+                  },
+                  messageId: 'unexpectedEnumsOrder',
+                },
+                {
+                  data: {
+                    right: 'B',
+                    left: 'Z',
+                  },
+                  messageId: 'extraSpacingBetweenEnumsMembers',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    {
+                      elementNamePattern: 'A',
+                      groupName: 'a',
+                    },
+                  ],
+                  groups: ['a', 'unknown'],
+                  newlinesBetween: 'never',
+                },
+              ],
+              code: dedent`
+                enum Enum {
+                  A = null,
+
+
+                 Y = null,
+                Z = null,
+
+                    B = null,
+                }
+              `,
+              output: dedent`
+                enum Enum {
+                  A = null,
+                 B = null,
+                Y = null,
+                    Z = null,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): keeps one newline when always`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'Z',
+                    left: 'A',
+                  },
+                  messageId: 'extraSpacingBetweenEnumsMembers',
+                },
+                {
+                  data: {
+                    right: 'Y',
+                    left: 'Z',
+                  },
+                  messageId: 'unexpectedEnumsOrder',
+                },
+                {
+                  data: {
+                    right: 'B',
+                    left: 'Y',
+                  },
+                  messageId: 'missedSpacingBetweenEnumsMembers',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    {
+                      elementNamePattern: 'A',
+                      groupName: 'a',
+                    },
+                    {
+                      elementNamePattern: 'B',
+                      groupName: 'b',
+                    },
+                  ],
+                  groups: ['a', 'unknown', 'b'],
+                  newlinesBetween: 'always',
+                },
+              ],
+              output: dedent`
+                enum Enum {
+                  A = null,
+
+                 Y = null,
+                Z = null,
+
+                    B = null,
+                }
+              `,
+              code: dedent`
+                enum Enum {
+                  A = null,
+
+
+                 Z = null,
+                Y = null,
+                    B = null,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`,
+        rule,
+        {
+          invalid: [
+            {
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    { elementNamePattern: 'A', groupName: 'a' },
+                    { elementNamePattern: 'B', groupName: 'b' },
+                    { elementNamePattern: 'C', groupName: 'c' },
+                    { elementNamePattern: 'D', groupName: 'd' },
+                    { elementNamePattern: 'E', groupName: 'e' },
+                  ],
+                  groups: [
+                    'a',
+                    { newlinesBetween: 'always' },
+                    'b',
+                    { newlinesBetween: 'always' },
+                    'c',
+                    { newlinesBetween: 'never' },
+                    'd',
+                    { newlinesBetween: 'ignore' },
+                    'e',
+                  ],
+                  newlinesBetween: 'always',
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    right: 'B',
+                    left: 'A',
+                  },
+                  messageId: 'missedSpacingBetweenEnumsMembers',
+                },
+                {
+                  data: {
+                    right: 'C',
+                    left: 'B',
+                  },
+                  messageId: 'extraSpacingBetweenEnumsMembers',
+                },
+                {
+                  data: {
+                    right: 'D',
+                    left: 'C',
+                  },
+                  messageId: 'extraSpacingBetweenEnumsMembers',
+                },
+              ],
+              output: dedent`
+                enum Enum {
+                  A = null,
+
+                  B = null,
+
+                  C = null,
+                  D = null,
+
+
+                  E = null,
+                }
+              `,
+              code: dedent`
+                enum Enum {
+                  A = null,
+                  B = null,
+
+
+                  C = null,
+
+                  D = null,
+
+
+                  E = null,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  enum Enum {
+                    A = null, // Comment after
+                    B = null,
+
+                    C = null,
+                  }
+                `,
+                dedent`
+                  enum Enum {
+                    A = null, // Comment after
+
+                    B = null,
+                    C = null,
+                  }
+                `,
+              ],
+              options: [
+                {
+                  customGroups: [
+                    {
+                      elementNamePattern: 'B|C',
+                      groupName: 'b|c',
+                    },
+                  ],
+                  groups: ['unknown', 'b|c'],
+                  newlinesBetween: 'always',
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'unknown',
+                    leftGroup: 'b|c',
+                    right: 'A',
+                    left: 'B',
+                  },
+                  messageId: 'unexpectedEnumsGroupOrder',
+                },
+              ],
+              code: dedent`
+                enum Enum {
+                  B = null,
+                  A = null, // Comment after
+
+                  C = null,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+    })
   })
 
   describe(`${ruleName}: sorting by natural order`, () => {
@@ -2020,6 +2623,7 @@ describe(ruleName, () => {
       'line-length',
       'natural',
     ]
+
     for (let type of sortTypes) {
       ruleTester.run(
         `${ruleName}: sortByValue = true => sorts numerical enums numerically for type ${type}`,
@@ -2443,6 +3047,42 @@ describe(ruleName, () => {
           valid: [],
         },
       )
+
+      ruleTester.run(
+        `${ruleName}: prioritizes dependencies over group configuration`,
+        rule,
+        {
+          valid: [
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      groupName: 'attributesStartingWithA',
+                      elementNamePattern: 'A',
+                    },
+                    {
+                      groupName: 'attributesStartingWithB',
+                      elementNamePattern: 'B',
+                    },
+                  ],
+                  groups: [
+                    'attributesStartingWithA',
+                    'attributesStartingWithB',
+                  ],
+                },
+              ],
+              code: dedent`
+                enum Enum {
+                  B = 'B',
+                  A = B,
+                }
+              `,
+            },
+          ],
+          invalid: [],
+        },
+      )
     })
 
     describe(`${ruleName}: handles complex comment cases`, () => {