Repository: azat-io/eslint-plugin-perfectionist
Pull request #499 (2025-03-26 16:56:38)

feat: add `groups`-related options to `sort-named-exports` and `sort-named-imports`
# Description

This PR adds the following options to `sort-named-exports` and `sort-named-imports`:

## `sort-named-exports`

- `groups` option
  - selectors: `export`
  - modifiers:
  	- `type`: matches type exports.
  	- `value`: matches value exports.
  - Deprecates `groupKind` for `groups` with the modifiers.
- `customGroups` option with `elementNamePattern` filter.
- `newlinesBetween` option.   

## `sort-named-imports`

- `groups` option
  - selectors: `import`
  - modifiers:
  	- `type`: matches type imports.
  	- `value`: matches value imports.
  - Deprecates `groupKind` for `groups` with the modifiers.
- `customGroups` option with `elementNamePattern` filter.
- `newlinesBetween` option.   

### What is the purpose of this pull request?

- [x] New Feature


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/azat-io/eslint-plugin-perfectionist/pull/499?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azat+S.) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`93a6f55`)](https://app.codecov.io/gh/azat-io/eslint-plugin-perfectionist/commit/93a6f557c9dba0019d1bca234d7edd93fb187132?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azat+S.) to head [(`3107915`)](https://app.codecov.io/gh/azat-io/eslint-plugin-perfectionist/commit/31079155b854d84b90e3abca9c9b06bdfe9669c6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azat+S.).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main      #499   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           91        92    +1     
  Lines         7593      7692   +99     
  Branches      1581      1571   -10     
=========================================
+ Hits          7593      7692   +99     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/azat-io/eslint-plugin-perfectionist/pull/499?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azat+S.).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Azat+S.).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>

Patch:
diff --git a/docs/content/rules/sort-array-includes.mdx b/docs/content/rules/sort-array-includes.mdx
index ecfa1da..bc7b56a 100644
--- a/docs/content/rules/sort-array-includes.mdx
+++ b/docs/content/rules/sort-array-includes.mdx
@@ -366,7 +366,7 @@ This feature is only applicable when `partitionByNewLine` is false.
 <sub>
   type: `Array<CustomGroupDefinition | CustomGroupAnyOfDefinition>`
 </sub>
-<sub>default: `{}`</sub>
+<sub>default: `[]`</sub>
 
 You can define your own groups and use regexp patterns to match specific object type members.
 
diff --git a/docs/content/rules/sort-enums.mdx b/docs/content/rules/sort-enums.mdx
index 7b66bd4..d8e258b 100644
--- a/docs/content/rules/sort-enums.mdx
+++ b/docs/content/rules/sort-enums.mdx
@@ -280,7 +280,7 @@ This feature is only applicable when `partitionByNewLine` is false.
 <sub>
   type: `Array<CustomGroupDefinition | CustomGroupAnyOfDefinition>`
 </sub>
-<sub>default: `{}`</sub>
+<sub>default: `[]`</sub>
 
 You can define your own groups and use regexp patterns to match specific elements.
 
diff --git a/docs/content/rules/sort-exports.mdx b/docs/content/rules/sort-exports.mdx
index c8dc749..d9c1ce3 100644
--- a/docs/content/rules/sort-exports.mdx
+++ b/docs/content/rules/sort-exports.mdx
@@ -282,7 +282,7 @@ This feature is only applicable when `partitionByNewLine` is false.
 <sub>
   type: `Array<CustomGroupDefinition | CustomGroupAnyOfDefinition>`
 </sub>
-<sub>default: `{}`</sub>
+<sub>default: `[]`</sub>
 
 You can define your own groups and use regexp patterns to match specific exports.
 
diff --git a/docs/content/rules/sort-maps.mdx b/docs/content/rules/sort-maps.mdx
index 567d1f1..aa30eca 100644
--- a/docs/content/rules/sort-maps.mdx
+++ b/docs/content/rules/sort-maps.mdx
@@ -310,7 +310,7 @@ This feature is only applicable when `partitionByNewLine` is false.
 <sub>
   type: `Array<CustomGroupDefinition | CustomGroupAnyOfDefinition>`
 </sub>
-<sub>default: `{}`</sub>
+<sub>default: `[]`</sub>
 
 You can define your own groups and use regexp patterns to match specific elements.
 
diff --git a/docs/content/rules/sort-named-exports.mdx b/docs/content/rules/sort-named-exports.mdx
index 6ffc6e9..fd9f44b 100644
--- a/docs/content/rules/sort-named-exports.mdx
+++ b/docs/content/rules/sort-named-exports.mdx
@@ -107,7 +107,7 @@ Specifies the sorting method.
 - `'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.
-- `'unsorted'` — Do not sort items.
+- `'unsorted'` — Do not sort items. [`grouping`](#groups) and [`newlines behavior`](#newlinesbetween) are still enforced.
 
 ### order
 
@@ -190,10 +190,12 @@ Determines whether to use the export alias as the name for sorting instead of th
 - `true` — Use the export alias for sorting.
 - `false` — Use the local name for sorting.
 
-### groupKind
+### [DEPRECATED] groupKind
 
 <sub>default: `'mixed'`</sub>
 
+Use the [groups](#groups) option with the `value` and `type` modifiers instead.
+
 Allows you to group named exports by their kind, determining whether value exports should come before or after type exports.
 
 - `mixed` — Do not group named exports by their kind; export statements are sorted together regardless of their type.
@@ -240,6 +242,138 @@ export {
 
 Each group of 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 groups.
+
+- `ignore` — Do not report errors related to new lines.
+- `always` — Enforce one new line between each group, and forbid new lines inside a group.
+- `never` — No new lines are allowed.
+
+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 named exports groups for sorting. Groups help organize named exports into categories, making them more readable and maintainable.
+
+Each named export will be assigned a single group specified in the `groups` option (or the `unknown` group if no match is found).
+The order of items in the `groups` option determines how groups are ordered.
+
+Within a given group, members will be sorted according to the `type`, `order`, `ignoreCase`, etc. options.
+
+Individual groups can be combined together by placing them in an array. The order of groups in that array does not matter.
+All members of the groups in the array will be sorted together as if they were part of a single group.
+
+Predefined groups are characterized by a single selector and potentially multiple modifiers. You may enter modifiers in any order, but the selector must always come at the end.
+
+#### Selectors
+
+The only selector possible for this rule is `export`.
+
+#### Modifiers
+
+- `value`: Matches value exports.
+- `type`: Matches type exports.
+
+Example: `type-export`.
+
+#### Important notes
+
+##### The `unknown` group
+
+Members that don’t fit into any group specified in the `groups` option will be placed in the `unknown` group. If the `unknown` group is not specified in the `groups` option,
+it will automatically be added to the end of the list.
+
+##### 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 named exports.
+
+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'
+  fallbackSort?: { type: string; order?: 'asc' | 'desc' }
+  newlinesInside?: 'always' | 'never'
+  selector?: string
+  elementNamePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
+}
+
+```
+A named export 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'
+  fallbackSort?: { type: string; order?: 'asc' | 'desc' }
+  newlinesInside?: 'always' | 'never'
+  anyOf: Array<{
+      selector?: string
+      elementNamePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
+  }>
+}
+```
+
+A named export will match a `CustomGroupAnyOfDefinition` group if it matches all the filters of at least one of the `anyOf` items.
+
+#### Attributes
+
+- `groupName`: The group's name, which needs to be put in the `groups` option.
+- `selector`: Filter on the `selector` of the element.
+- `elementNamePattern`: If entered, will check that the name of the element matches the pattern entered.
+- `type`: Overrides the sort type for that custom group. `unsorted` will not sort the group.
+- `order`: Overrides the sort order for that custom group
+- `fallbackSort`: Overrides the fallbackSort option for that custom group
+- `newlinesInside`: Enforces a specific newline behavior between elements of the 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
@@ -267,6 +401,9 @@ Each group of members (separated by empty lines) is treated independently, and t
                   groupKind: 'mixed',
                   partitionByNewLine: false,
                   partitionByComment: false,
+                  newlinesBetween: 'ignore',
+                  groups: [],
+                  customGroups: [],
                 },
               ],
             },
@@ -296,6 +433,9 @@ Each group of members (separated by empty lines) is treated independently, and t
                 groupKind: 'mixed',
                 partitionByNewLine: false,
                 partitionByComment: false,
+                newlinesBetween: 'ignore',
+                groups: [],
+                customGroups: [],
               },
             ],
           },
diff --git a/docs/content/rules/sort-named-imports.mdx b/docs/content/rules/sort-named-imports.mdx
index 51ee7df..8130e41 100644
--- a/docs/content/rules/sort-named-imports.mdx
+++ b/docs/content/rules/sort-named-imports.mdx
@@ -108,7 +108,7 @@ Specifies the sorting method.
 - `'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.
-- `'unsorted'` — Do not sort items.
+- `'unsorted'` — Do not sort items. [`grouping`](#groups) and [`newlines behavior`](#newlinesbetween) are still enforced.
 
 ### order
 
@@ -191,13 +191,15 @@ Determines whether to use the import alias as the name for sorting instead of th
 - `true` — Use the import alias for sorting.
 - `false` — Use the exported name for sorting.
 
-### groupKind
+### [DEPRECATED] groupKind
 
 <sub>default: `'mixed'`</sub>
 
+Use the [groups](#groups) option with the `value` and `type` modifiers instead.
+
 Allows you to group named imports by their kind, determining whether value imports should come before or after type imports.
 
-- `mixed` — Do not group named imports by their kind; export statements are sorted together regardless of their type.
+- `mixed` — Do not group named imports by their kind; named imports statements are sorted together regardless of their type.
 - `values-first` — Group all value imports before type imports.
 - `types-first` — Group all type imports before value imports.
 
@@ -239,6 +241,137 @@ import {
 } from './devices'
 ```
 
+### newlinesBetween
+
+<sub>default: `'ignore'`</sub>
+
+Specifies how new lines should be handled between groups.
+
+- `ignore` — Do not report errors related to new lines.
+- `always` — Enforce one new line between each group, and forbid new lines inside a group.
+- `never` — No new lines are allowed.
+
+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 named imports groups for sorting. Groups help organize named imports into categories, making them more readable and maintainable.
+
+Each named import will be assigned a single group specified in the `groups` option (or the `unknown` group if no match is found).
+The order of items in the `groups` option determines how groups are ordered.
+
+Within a given group, members will be sorted according to the `type`, `order`, `ignoreCase`, etc. options.
+
+Individual groups can be combined together by placing them in an array. The order of groups in that array does not matter.
+All members of the groups in the array will be sorted together as if they were part of a single group.
+
+Predefined groups are characterized by a single selector and potentially multiple modifiers. You may enter modifiers in any order, but the selector must always come at the end.
+
+#### Selectors
+
+The only selector possible for this rule is `import`.
+
+#### Modifiers
+
+- `value`: Matches value imports.
+- `type`: Matches type imports.
+
+Example: `type-import`.
+
+#### Important notes
+
+##### The `unknown` group
+
+Members that don’t fit into any group specified in the `groups` option will be placed in the `unknown` group. If the `unknown` group is not specified in the `groups` option,
+it will automatically be added to the end of the list.
+
+##### 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 named imports.
+
+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'
+  fallbackSort?: { type: string; order?: 'asc' | 'desc' }
+  newlinesInside?: 'always' | 'never'
+  selector?: string
+  elementNamePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
+}
+
+```
+A named import 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'
+  fallbackSort?: { type: string; order?: 'asc' | 'desc' }
+  newlinesInside?: 'always' | 'never'
+  anyOf: Array<{
+      selector?: string
+      elementNamePattern?: string | string[] | { pattern: string; flags?: string } | { pattern: string; flags?: string }[]
+  }>
+}
+```
+
+A named import will match a `CustomGroupAnyOfDefinition` group if it matches all the filters of at least one of the `anyOf` items.
+
+#### Attributes
+
+- `groupName`: The group's name, which needs to be put in the `groups` option.
+- `selector`: Filter on the `selector` of the element.
+- `elementNamePattern`: If entered, will check that the name of the element matches the pattern entered.
+- `type`: Overrides the sort type for that custom group. `unsorted` will not sort the group.
+- `order`: Overrides the sort order for that custom group
+- `fallbackSort`: Overrides the fallbackSort option for that custom group
+- `newlinesInside`: Enforces a specific newline behavior between elements of the 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
 
@@ -267,6 +400,9 @@ import {
                   groupKind: 'mixed',
                   partitionByNewLine: false,
                   partitionByComment: false,
+                  newlinesBetween: 'ignore',
+                  groups: [],
+                  customGroups: [],
                 },
               ],
             },
@@ -296,6 +432,9 @@ import {
                 groupKind: 'mixed',
                 partitionByNewLine: false,
                 partitionByComment: false,
+                newlinesBetween: 'ignore',
+                groups: [],
+                customGroups: [],
               },
             ],
           },
diff --git a/docs/content/rules/sort-sets.mdx b/docs/content/rules/sort-sets.mdx
index 283c25a..a5f04aa 100644
--- a/docs/content/rules/sort-sets.mdx
+++ b/docs/content/rules/sort-sets.mdx
@@ -325,7 +325,7 @@ This feature is only applicable when `partitionByNewLine` is false.
 <sub>
   type: `Array<CustomGroupDefinition | CustomGroupAnyOfDefinition>`
 </sub>
-<sub>default: `{}`</sub>
+<sub>default: `[]`</sub>
 
 You can define your own groups and use regexp patterns to match specific elements.
 
diff --git a/rules/sort-jsx-props.ts b/rules/sort-jsx-props.ts
index c116213..68e410f 100644
--- a/rules/sort-jsx-props.ts
+++ b/rules/sort-jsx-props.ts
@@ -25,10 +25,10 @@ import { buildGetCustomGroupOverriddenOptionsFunction } from '../utils/get-custo
 import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration'
 import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration'
 import { getMatchingContextOptions } from '../utils/get-matching-context-options'
-import { doesCustomGroupMatch } from './sort-jsx-props/does-custom-group-match'
 import { generatePredefinedGroups } from '../utils/generate-predefined-groups'
 import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
 import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
+import { doesCustomGroupMatch } from '../utils/does-custom-group-match'
 import { singleCustomGroupJsonSchema } from './sort-jsx-props/types'
 import { allModifiers, allSelectors } from './sort-jsx-props/types'
 import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
diff --git a/rules/sort-jsx-props/does-custom-group-match.ts b/rules/sort-jsx-props/does-custom-group-match.ts
deleted file mode 100644
index f5d9d54..0000000
--- a/rules/sort-jsx-props/does-custom-group-match.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import type { SingleCustomGroup, Selector, Modifier } from './types'
-import type { AnyOfCustomGroup } from '../../types/common-options'
-
-import { matches } from '../../utils/matches'
-
-interface DoesCustomGroupMatchParameters {
-  customGroup: AnyOfCustomGroup<SingleCustomGroup> | SingleCustomGroup
-  elementValue: string | null
-  selectors: Selector[]
-  modifiers: Modifier[]
-  elementName: string
-}
-
-/**
- * Determines whether a custom group matches the given properties.
- * @param {DoesCustomGroupMatchParameters} props - The properties to compare
- * with the custom group, including selectors, modifiers, and element name.
- * @returns {boolean} `true` if the custom group matches the properties;
- * otherwise, `false`.
- */
-export let doesCustomGroupMatch = (
-  props: DoesCustomGroupMatchParameters,
-): boolean => {
-  if ('anyOf' in props.customGroup) {
-    // At least one subgroup must match
-    return props.customGroup.anyOf.some(subgroup =>
-      doesCustomGroupMatch({ ...props, customGroup: subgroup }),
-    )
-  }
-  if (
-    props.customGroup.selector &&
-    !props.selectors.includes(props.customGroup.selector)
-  ) {
-    return false
-  }
-
-  if (props.customGroup.modifiers) {
-    for (let modifier of props.customGroup.modifiers) {
-      if (!props.modifiers.includes(modifier)) {
-        return false
-      }
-    }
-  }
-
-  if (
-    'elementNamePattern' in props.customGroup &&
-    props.customGroup.elementNamePattern
-  ) {
-    let matchesElementNamePattern: boolean = matches(
-      props.elementName,
-      props.customGroup.elementNamePattern,
-    )
-    if (!matchesElementNamePattern) {
-      return false
-    }
-  }
-
-  if (
-    'elementValuePattern' in props.customGroup &&
-    props.customGroup.elementValuePattern
-  ) {
-    let matchesElementNamePattern: boolean = matches(
-      props.elementValue ?? '',
-      props.customGroup.elementValuePattern,
-    )
-    if (!matchesElementNamePattern) {
-      return false
-    }
-  }
-
-  return true
-}
diff --git a/rules/sort-named-exports.ts b/rules/sort-named-exports.ts
index 3a891a4..4ac8594 100644
--- a/rules/sort-named-exports.ts
+++ b/rules/sort-named-exports.ts
@@ -1,59 +1,73 @@
-import type { TSESTree } from '@typescript-eslint/types'
-
 import type {
-  PartitionByCommentOption,
-  CommonOptions,
-} from '../types/common-options'
-import type { SortingNode } from '../types/sorting-node'
+  SortNamedExportsSortingNode,
+  Modifier,
+  Selector,
+  Options,
+} from './sort-named-exports/types'
 
 import {
+  buildCustomGroupsArrayJsonSchema,
   partitionByCommentJsonSchema,
   partitionByNewLineJsonSchema,
+  newlinesBetweenJsonSchema,
   commonJsonSchemas,
+  groupsJsonSchema,
 } from '../utils/common-json-schemas'
+import {
+  MISSED_SPACING_ERROR,
+  EXTRA_SPACING_ERROR,
+  GROUP_ORDER_ERROR,
+  ORDER_ERROR,
+} from '../utils/report-errors'
+import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
+import {
+  singleCustomGroupJsonSchema,
+  allModifiers,
+  allSelectors,
+} from './sort-named-exports/types'
+import { buildGetCustomGroupOverriddenOptionsFunction } from '../utils/get-custom-groups-compare-options'
+import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration'
 import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration'
+import { generatePredefinedGroups } from '../utils/generate-predefined-groups'
 import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
 import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
+import { doesCustomGroupMatch } from '../utils/does-custom-group-match'
+import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
 import { createEslintRule } from '../utils/create-eslint-rule'
 import { reportAllErrors } from '../utils/report-all-errors'
 import { shouldPartition } from '../utils/should-partition'
+import { computeGroup } from '../utils/compute-group'
 import { rangeToDiff } from '../utils/range-to-diff'
-import { ORDER_ERROR } from '../utils/report-errors'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { sortNodes } from '../utils/sort-nodes'
 import { complete } from '../utils/complete'
 
-type Options = [
-  Partial<
-    {
-      groupKind: 'values-first' | 'types-first' | 'mixed'
-      partitionByComment: PartitionByCommentOption
-      partitionByNewLine: boolean
-      ignoreAlias: boolean
-    } & CommonOptions
-  >,
-]
-
-interface SortNamedExportsSortingNode
-  extends SortingNode<TSESTree.ExportSpecifier> {
-  groupKind: 'value' | 'type'
-}
+type MESSAGE_ID =
+  | 'unexpectedNamedExportsGroupOrder'
+  | 'missedSpacingBetweenNamedExports'
+  | 'extraSpacingBetweenNamedExports'
+  | 'unexpectedNamedExportsOrder'
 
-type MESSAGE_ID = 'unexpectedNamedExportsOrder'
+/**
+ * Cache computed groups by modifiers and selectors for performance
+ */
+let cachedGroupsByModifiersAndSelectors = new Map<string, string[]>()
 
 let defaultOptions: Required<Options[0]> = {
   fallbackSort: { type: 'unsorted' },
   specialCharacters: 'keep',
   partitionByNewLine: false,
   partitionByComment: false,
+  newlinesBetween: 'ignore',
   type: 'alphabetical',
   ignoreAlias: false,
   groupKind: 'mixed',
+  customGroups: [],
   ignoreCase: true,
   locales: 'en-US',
   alphabet: '',
   order: 'asc',
+  groups: [],
 }
 
 export default createEslintRule<Options, MESSAGE_ID>({
@@ -66,6 +80,12 @@ export default createEslintRule<Options, MESSAGE_ID>({
       let settings = getSettings(context.settings)
       let options = complete(context.options.at(0), settings, defaultOptions)
       validateCustomSortConfiguration(options)
+      validateGeneratedGroupsConfiguration({
+        modifiers: allModifiers,
+        selectors: allSelectors,
+        options,
+      })
+      validateNewlinesAndPartitionConfiguration(options)
 
       let { sourceCode, id } = context
       let eslintDisabledLines = getEslintDisabledLines({
@@ -94,7 +114,31 @@ export default createEslintRule<Options, MESSAGE_ID>({
           }
         }
 
-        let lastSortingNode = formattedMembers.at(-1)?.at(-1)
+        let selector: Selector = 'export'
+        let modifiers: Modifier[] = []
+        if (specifier.exportKind === 'value') {
+          modifiers.push('value')
+        } else {
+          modifiers.push('type')
+        }
+
+        let predefinedGroups = generatePredefinedGroups({
+          cache: cachedGroupsByModifiersAndSelectors,
+          selectors: [selector],
+          modifiers,
+        })
+        let group = computeGroup({
+          customGroupMatcher: customGroup =>
+            doesCustomGroupMatch({
+              selectors: [selector],
+              elementName: name,
+              customGroup,
+              modifiers,
+            }),
+          predefinedGroups,
+          options,
+        })
+
         let sortingNode: SortNamedExportsSortingNode = {
           isEslintDisabled: isNodeEslintDisabled(
             specifier,
@@ -103,9 +147,11 @@ export default createEslintRule<Options, MESSAGE_ID>({
           groupKind: specifier.exportKind === 'type' ? 'type' : 'value',
           size: rangeToDiff(specifier, sourceCode),
           node: specifier,
+          group,
           name,
         }
 
+        let lastSortingNode = formattedMembers.at(-1)?.at(-1)
         if (
           shouldPartition({
             lastSortingNode,
@@ -140,15 +186,20 @@ export default createEslintRule<Options, MESSAGE_ID>({
           ignoreEslintDisabledNodes: boolean,
         ): SortNamedExportsSortingNode[] =>
           filteredGroupKindNodes.flatMap(groupedNodes =>
-            sortNodes({
+            sortNodesByGroups({
+              getOptionsByGroupNumber:
+                buildGetCustomGroupOverriddenOptionsFunction(options),
               ignoreEslintDisabledNodes,
+              groups: options.groups,
               nodes: groupedNodes,
-              options,
             }),
           )
 
         reportAllErrors<MESSAGE_ID>({
           availableMessageIds: {
+            missedSpacingBetweenMembers: 'missedSpacingBetweenNamedExports',
+            extraSpacingBetweenMembers: 'extraSpacingBetweenNamedExports',
+            unexpectedGroupOrder: 'unexpectedNamedExportsGroupOrder',
             unexpectedOrder: 'unexpectedNamedExportsOrder',
           },
           sortNodesExcludingEslintDisabled,
@@ -161,34 +212,44 @@ export default createEslintRule<Options, MESSAGE_ID>({
     },
   }),
   meta: {
-    schema: [
-      {
+    schema: {
+      items: {
         properties: {
           ...commonJsonSchemas,
           groupKind: {
+            description: '[DEPRECATED] Specifies top-level groups.',
             enum: ['mixed', 'values-first', 'types-first'],
-            description: 'Specifies top-level groups.',
             type: 'string',
           },
           ignoreAlias: {
             description: 'Controls whether to ignore alias names.',
             type: 'boolean',
           },
+          customGroups: buildCustomGroupsArrayJsonSchema({
+            singleCustomGroupJsonSchema,
+          }),
           partitionByComment: partitionByCommentJsonSchema,
           partitionByNewLine: partitionByNewLineJsonSchema,
+          newlinesBetween: newlinesBetweenJsonSchema,
+          groups: groupsJsonSchema,
         },
         additionalProperties: false,
         type: 'object',
       },
-    ],
+      uniqueItems: true,
+      type: 'array',
+    },
+    messages: {
+      missedSpacingBetweenNamedExports: MISSED_SPACING_ERROR,
+      extraSpacingBetweenNamedExports: EXTRA_SPACING_ERROR,
+      unexpectedNamedExportsGroupOrder: GROUP_ORDER_ERROR,
+      unexpectedNamedExportsOrder: ORDER_ERROR,
+    },
     docs: {
       url: 'https://perfectionist.dev/rules/sort-named-exports',
       description: 'Enforce sorted named exports.',
       recommended: true,
     },
-    messages: {
-      unexpectedNamedExportsOrder: ORDER_ERROR,
-    },
     type: 'suggestion',
     fixable: 'code',
   },
diff --git a/rules/sort-named-exports/types.ts b/rules/sort-named-exports/types.ts
new file mode 100644
index 0000000..dddb6ca
--- /dev/null
+++ b/rules/sort-named-exports/types.ts
@@ -0,0 +1,69 @@
+import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'
+import type { TSESTree } from '@typescript-eslint/types'
+
+import type {
+  PartitionByCommentOption,
+  NewlinesBetweenOption,
+  CustomGroupsOption,
+  CommonOptions,
+  GroupsOptions,
+  RegexOption,
+} from '../../types/common-options'
+import type { JoinWithDash } from '../../types/join-with-dash'
+import type { SortingNode } from '../../types/sorting-node'
+
+import {
+  buildCustomGroupModifiersJsonSchema,
+  buildCustomGroupSelectorJsonSchema,
+  regexJsonSchema,
+} from '../../utils/common-json-schemas'
+
+export type Options = Partial<
+  {
+    /**
+     * @deprecated for {@link `groups`}
+     */
+    groupKind: 'values-first' | 'types-first' | 'mixed'
+    customGroups: CustomGroupsOption<SingleCustomGroup>
+    partitionByComment: PartitionByCommentOption
+    newlinesBetween: NewlinesBetweenOption
+    groups: GroupsOptions<Group>
+    partitionByNewLine: boolean
+    ignoreAlias: boolean
+  } & CommonOptions
+>[]
+
+export interface SortNamedExportsSortingNode
+  extends SortingNode<TSESTree.ExportSpecifier> {
+  groupKind: 'value' | 'type'
+}
+
+export type SingleCustomGroup = {
+  modifiers?: Modifier[]
+  selector?: Selector
+} & {
+  elementNamePattern?: RegexOption
+}
+
+export type Modifier = ValueModifier | TypeModifier
+
+export type Selector = ExportSelector
+
+type ExportGroup = JoinWithDash<[ValueModifier, TypeModifier, ExportSelector]>
+
+type Group = ExportGroup | 'unknown' | string
+
+type ExportSelector = 'export'
+
+type ValueModifier = 'value'
+
+type TypeModifier = 'type'
+
+export let allSelectors: Selector[] = ['export']
+export let allModifiers: Modifier[] = ['value', 'type']
+
+export let singleCustomGroupJsonSchema: Record<string, JSONSchema4> = {
+  modifiers: buildCustomGroupModifiersJsonSchema(allModifiers),
+  selector: buildCustomGroupSelectorJsonSchema(allSelectors),
+  elementNamePattern: regexJsonSchema,
+}
diff --git a/rules/sort-named-imports.ts b/rules/sort-named-imports.ts
index 45e7cf6..e9f85b8 100644
--- a/rules/sort-named-imports.ts
+++ b/rules/sort-named-imports.ts
@@ -1,59 +1,73 @@
-import type { TSESTree } from '@typescript-eslint/types'
-
 import type {
-  PartitionByCommentOption,
-  CommonOptions,
-} from '../types/common-options'
-import type { SortingNode } from '../types/sorting-node'
+  SortNamedImportsSortingNode,
+  Modifier,
+  Selector,
+  Options,
+} from './sort-named-imports/types'
 
 import {
+  buildCustomGroupsArrayJsonSchema,
   partitionByCommentJsonSchema,
   partitionByNewLineJsonSchema,
+  newlinesBetweenJsonSchema,
   commonJsonSchemas,
+  groupsJsonSchema,
 } from '../utils/common-json-schemas'
+import {
+  MISSED_SPACING_ERROR,
+  EXTRA_SPACING_ERROR,
+  GROUP_ORDER_ERROR,
+  ORDER_ERROR,
+} from '../utils/report-errors'
+import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
+import {
+  singleCustomGroupJsonSchema,
+  allModifiers,
+  allSelectors,
+} from './sort-named-imports/types'
+import { buildGetCustomGroupOverriddenOptionsFunction } from '../utils/get-custom-groups-compare-options'
+import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration'
 import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration'
+import { generatePredefinedGroups } from '../utils/generate-predefined-groups'
 import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
 import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
+import { doesCustomGroupMatch } from '../utils/does-custom-group-match'
+import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
 import { createEslintRule } from '../utils/create-eslint-rule'
 import { reportAllErrors } from '../utils/report-all-errors'
 import { shouldPartition } from '../utils/should-partition'
+import { computeGroup } from '../utils/compute-group'
 import { rangeToDiff } from '../utils/range-to-diff'
-import { ORDER_ERROR } from '../utils/report-errors'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { sortNodes } from '../utils/sort-nodes'
 import { complete } from '../utils/complete'
 
-type Options = [
-  Partial<
-    {
-      groupKind: 'values-first' | 'types-first' | 'mixed'
-      partitionByComment: PartitionByCommentOption
-      partitionByNewLine: boolean
-      ignoreAlias: boolean
-    } & CommonOptions
-  >,
-]
-
-interface SortNamedImportsSortingNode
-  extends SortingNode<TSESTree.ImportClause> {
-  groupKind: 'value' | 'type'
-}
+type MESSAGE_ID =
+  | 'unexpectedNamedImportsGroupOrder'
+  | 'missedSpacingBetweenNamedImports'
+  | 'extraSpacingBetweenNamedImports'
+  | 'unexpectedNamedImportsOrder'
 
-type MESSAGE_ID = 'unexpectedNamedImportsOrder'
+/**
+ * Cache computed groups by modifiers and selectors for performance
+ */
+let cachedGroupsByModifiersAndSelectors = new Map<string, string[]>()
 
 let defaultOptions: Required<Options[0]> = {
   fallbackSort: { type: 'unsorted' },
   specialCharacters: 'keep',
   partitionByNewLine: false,
   partitionByComment: false,
+  newlinesBetween: 'ignore',
   type: 'alphabetical',
   ignoreAlias: false,
   groupKind: 'mixed',
+  customGroups: [],
   ignoreCase: true,
   locales: 'en-US',
   alphabet: '',
   order: 'asc',
+  groups: [],
 }
 
 export default createEslintRule<Options, MESSAGE_ID>({
@@ -69,6 +83,12 @@ export default createEslintRule<Options, MESSAGE_ID>({
       let settings = getSettings(context.settings)
       let options = complete(context.options.at(0), settings, defaultOptions)
       validateCustomSortConfiguration(options)
+      validateGeneratedGroupsConfiguration({
+        modifiers: allModifiers,
+        selectors: allSelectors,
+        options,
+      })
+      validateNewlinesAndPartitionConfiguration(options)
 
       let { sourceCode, id } = context
       let eslintDisabledLines = getEslintDisabledLines({
@@ -88,7 +108,34 @@ export default createEslintRule<Options, MESSAGE_ID>({
           }
         }
 
-        let lastSortingNode = formattedMembers.at(-1)?.at(-1)
+        let selector: Selector = 'import'
+        let modifiers: Modifier[] = []
+        if (
+          specifier.type === 'ImportSpecifier' &&
+          specifier.importKind === 'type'
+        ) {
+          modifiers.push('type')
+        } else {
+          modifiers.push('value')
+        }
+
+        let predefinedGroups = generatePredefinedGroups({
+          cache: cachedGroupsByModifiersAndSelectors,
+          selectors: [selector],
+          modifiers,
+        })
+        let group = computeGroup({
+          customGroupMatcher: customGroup =>
+            doesCustomGroupMatch({
+              selectors: [selector],
+              elementName: name,
+              customGroup,
+              modifiers,
+            }),
+          predefinedGroups,
+          options,
+        })
+
         let sortingNode: SortNamedImportsSortingNode = {
           groupKind:
             specifier.type === 'ImportSpecifier' &&
@@ -101,9 +148,11 @@ export default createEslintRule<Options, MESSAGE_ID>({
           ),
           size: rangeToDiff(specifier, sourceCode),
           node: specifier,
+          group,
           name,
         }
 
+        let lastSortingNode = formattedMembers.at(-1)?.at(-1)
         if (
           shouldPartition({
             lastSortingNode,
@@ -138,15 +187,20 @@ export default createEslintRule<Options, MESSAGE_ID>({
           ignoreEslintDisabledNodes: boolean,
         ): SortNamedImportsSortingNode[] =>
           filteredGroupKindNodes.flatMap(groupedNodes =>
-            sortNodes({
+            sortNodesByGroups({
+              getOptionsByGroupNumber:
+                buildGetCustomGroupOverriddenOptionsFunction(options),
               ignoreEslintDisabledNodes,
+              groups: options.groups,
               nodes: groupedNodes,
-              options,
             }),
           )
 
         reportAllErrors<MESSAGE_ID>({
           availableMessageIds: {
+            missedSpacingBetweenMembers: 'missedSpacingBetweenNamedImports',
+            extraSpacingBetweenMembers: 'extraSpacingBetweenNamedImports',
+            unexpectedGroupOrder: 'unexpectedNamedImportsGroupOrder',
             unexpectedOrder: 'unexpectedNamedImportsOrder',
           },
           sortNodesExcludingEslintDisabled,
@@ -159,34 +213,44 @@ export default createEslintRule<Options, MESSAGE_ID>({
     },
   }),
   meta: {
-    schema: [
-      {
+    schema: {
+      items: {
         properties: {
           ...commonJsonSchemas,
           groupKind: {
+            description: '[DEPRECATED] Specifies top-level groups.',
             enum: ['mixed', 'values-first', 'types-first'],
-            description: 'Specifies top-level groups.',
             type: 'string',
           },
           ignoreAlias: {
             description: 'Controls whether to ignore alias names.',
             type: 'boolean',
           },
+          customGroups: buildCustomGroupsArrayJsonSchema({
+            singleCustomGroupJsonSchema,
+          }),
           partitionByComment: partitionByCommentJsonSchema,
           partitionByNewLine: partitionByNewLineJsonSchema,
+          newlinesBetween: newlinesBetweenJsonSchema,
+          groups: groupsJsonSchema,
         },
         additionalProperties: false,
         type: 'object',
       },
-    ],
+      uniqueItems: true,
+      type: 'array',
+    },
+    messages: {
+      missedSpacingBetweenNamedImports: MISSED_SPACING_ERROR,
+      extraSpacingBetweenNamedImports: EXTRA_SPACING_ERROR,
+      unexpectedNamedImportsGroupOrder: GROUP_ORDER_ERROR,
+      unexpectedNamedImportsOrder: ORDER_ERROR,
+    },
     docs: {
       url: 'https://perfectionist.dev/rules/sort-named-imports',
       description: 'Enforce sorted named imports.',
       recommended: true,
     },
-    messages: {
-      unexpectedNamedImportsOrder: ORDER_ERROR,
-    },
     type: 'suggestion',
     fixable: 'code',
   },
diff --git a/rules/sort-named-imports/types.ts b/rules/sort-named-imports/types.ts
new file mode 100644
index 0000000..65642e9
--- /dev/null
+++ b/rules/sort-named-imports/types.ts
@@ -0,0 +1,68 @@
+import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'
+import type { TSESTree } from '@typescript-eslint/types'
+
+import type {
+  PartitionByCommentOption,
+  NewlinesBetweenOption,
+  CustomGroupsOption,
+  CommonOptions,
+  GroupsOptions,
+  RegexOption,
+} from '../../types/common-options'
+import type { JoinWithDash } from '../../types/join-with-dash'
+import type { SortingNode } from '../../types/sorting-node'
+
+import {
+  buildCustomGroupModifiersJsonSchema,
+  buildCustomGroupSelectorJsonSchema,
+  regexJsonSchema,
+} from '../../utils/common-json-schemas'
+
+export type Options = Partial<
+  {
+    /**
+     * @deprecated for {@link `groups`}
+     */
+    groupKind: 'values-first' | 'types-first' | 'mixed'
+    customGroups: CustomGroupsOption<SingleCustomGroup>
+    partitionByComment: PartitionByCommentOption
+    newlinesBetween: NewlinesBetweenOption
+    groups: GroupsOptions<Group>
+    partitionByNewLine: boolean
+    ignoreAlias: boolean
+  } & CommonOptions
+>[]
+
+export interface SortNamedImportsSortingNode
+  extends SortingNode<TSESTree.ImportClause> {
+  groupKind: 'value' | 'type'
+}
+
+export type SingleCustomGroup = {
+  modifiers?: Modifier[]
+  selector?: Selector
+} & {
+  elementNamePattern?: RegexOption
+}
+export type Modifier = ValueModifier | TypeModifier
+
+export type Selector = ImportSelector
+
+type ImportGroup = JoinWithDash<[ValueModifier, TypeModifier, ImportSelector]>
+
+type Group = ImportGroup | 'unknown' | string
+
+type ImportSelector = 'import'
+
+type ValueModifier = 'value'
+
+type TypeModifier = 'type'
+
+export let allSelectors: Selector[] = ['import']
+export let allModifiers: Modifier[] = ['value', 'type']
+
+export let singleCustomGroupJsonSchema: Record<string, JSONSchema4> = {
+  modifiers: buildCustomGroupModifiersJsonSchema(allModifiers),
+  selector: buildCustomGroupSelectorJsonSchema(allSelectors),
+  elementNamePattern: regexJsonSchema,
+}


Test patch:
diff --git a/test/rules/sort-named-exports.test.ts b/test/rules/sort-named-exports.test.ts
index 87ff591..2776b2b 100644
--- a/test/rules/sort-named-exports.test.ts
+++ b/test/rules/sort-named-exports.test.ts
@@ -842,6 +842,857 @@ describe(ruleName, () => {
       ],
       valid: [],
     })
+
+    describe(`${ruleName}: custom groups`, () => {
+      ruleTester.run(`${ruleName}: filters on modifier`, rule, {
+        invalid: [
+          {
+            errors: [
+              {
+                data: {
+                  rightGroup: 'typeElements',
+                  leftGroup: 'unknown',
+                  right: 'b',
+                  left: 'a',
+                },
+                messageId: 'unexpectedNamedExportsGroupOrder',
+              },
+            ],
+            options: [
+              {
+                customGroups: [
+                  {
+                    groupName: 'typeElements',
+                    modifiers: ['type'],
+                  },
+                ],
+                groups: ['typeElements', 'unknown'],
+              },
+            ],
+            output: dedent`
+              export {
+                type b,
+                a,
+              }
+            `,
+            code: dedent`
+              export {
+                a,
+                type b,
+              }
+            `,
+          },
+        ],
+        valid: [],
+      })
+
+      for (let elementNamePattern of [
+        'hello',
+        ['noMatch', 'hello'],
+        { pattern: 'HELLO', flags: 'i' },
+        ['noMatch', { pattern: 'HELLO', flags: 'i' }],
+      ]) {
+        ruleTester.run(`${ruleName}: filters on elementNamePattern`, rule, {
+          invalid: [
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      groupName: 'typesStartingWithHello',
+                      modifiers: ['type'],
+                      elementNamePattern,
+                    },
+                  ],
+                  groups: ['typesStartingWithHello', 'unknown'],
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'typesStartingWithHello',
+                    leftGroup: 'unknown',
+                    right: 'helloType',
+                    left: 'b',
+                  },
+                  messageId: 'unexpectedNamedExportsGroupOrder',
+                },
+              ],
+              output: dedent`
+                export {
+                  type helloType,
+                  a,
+                  b,
+                }
+              `,
+              code: dedent`
+                export {
+                  a,
+                  b,
+                  type helloType,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        })
+      }
+
+      ruleTester.run(
+        `${ruleName}: sort custom groups by overriding 'type' and 'order'`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'bb',
+                    left: 'a',
+                  },
+                  messageId: 'unexpectedNamedExportsOrder',
+                },
+                {
+                  data: {
+                    right: 'ccc',
+                    left: 'bb',
+                  },
+                  messageId: 'unexpectedNamedExportsOrder',
+                },
+                {
+                  data: {
+                    right: 'dddd',
+                    left: 'ccc',
+                  },
+                  messageId: 'unexpectedNamedExportsOrder',
+                },
+                {
+                  data: {
+                    rightGroup: 'reversedTypesByLineLength',
+                    leftGroup: 'unknown',
+                    right: 'eee',
+                    left: 'm',
+                  },
+                  messageId: 'unexpectedNamedExportsGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  customGroups: [
+                    {
+                      groupName: 'reversedTypesByLineLength',
+                      modifiers: ['type'],
+                      type: 'line-length',
+                      order: 'desc',
+                    },
+                  ],
+                  groups: ['reversedTypesByLineLength', 'unknown'],
+                  type: 'alphabetical',
+                  order: 'asc',
+                },
+              ],
+              output: dedent`
+                export {
+                  type dddd,
+                  type ccc,
+                  type eee,
+                  type bb,
+                  type ff,
+                  type a,
+                  type g,
+                  m,
+                  o,
+                  p,
+                }
+              `,
+              code: dedent`
+                export {
+                  type a,
+                  type bb,
+                  type ccc,
+                  type dddd,
+                  m,
+                  type eee,
+                  type ff,
+                  type g,
+                  o,
+                  p,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}: sort custom groups by overriding 'fallbackSort'`,
+        rule,
+        {
+          invalid: [
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      fallbackSort: {
+                        type: 'alphabetical',
+                        order: 'asc',
+                      },
+                      elementNamePattern: '^foo',
+                      type: 'line-length',
+                      groupName: 'foo',
+                      order: 'desc',
+                    },
+                  ],
+                  type: 'alphabetical',
+                  groups: ['foo'],
+                  order: 'asc',
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    right: 'fooBar',
+                    left: 'fooZar',
+                  },
+                  messageId: 'unexpectedNamedExportsOrder',
+                },
+              ],
+              output: dedent`
+                export {
+                  fooBar,
+                  fooZar,
+                }
+              `,
+              code: dedent`
+                export {
+                  fooZar,
+                  fooBar,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}: does not sort custom groups with 'unsorted' type`,
+        rule,
+        {
+          invalid: [
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      groupName: 'unsortedTypes',
+                      modifiers: ['type'],
+                      type: 'unsorted',
+                    },
+                  ],
+                  groups: ['unsortedTypes', 'unknown'],
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'unsortedTypes',
+                    leftGroup: 'unknown',
+                    right: 'c',
+                    left: 'm',
+                  },
+                  messageId: 'unexpectedNamedExportsGroupOrder',
+                },
+              ],
+              output: dedent`
+                export {
+                  type b,
+                  type a,
+                  type d,
+                  type e,
+                  type c,
+                  m,
+                }
+              `,
+              code: dedent`
+                export {
+                  type b,
+                  type a,
+                  type d,
+                  type e,
+                  m,
+                  type c,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(`${ruleName}: sort custom group blocks`, rule, {
+        invalid: [
+          {
+            options: [
+              {
+                customGroups: [
+                  {
+                    anyOf: [
+                      {
+                        elementNamePattern: 'foo|Foo',
+                        modifiers: ['type'],
+                      },
+                      {
+                        elementNamePattern: 'foo|Foo',
+                      },
+                    ],
+                    groupName: 'elementsIncludingFoo',
+                  },
+                ],
+                groups: ['elementsIncludingFoo', 'unknown'],
+              },
+            ],
+            errors: [
+              {
+                data: {
+                  rightGroup: 'elementsIncludingFoo',
+                  leftGroup: 'unknown',
+                  right: 'cFoo',
+                  left: 'a',
+                },
+                messageId: 'unexpectedNamedExportsGroupOrder',
+              },
+            ],
+            output: dedent`
+              export {
+                type cFoo,
+                foo,
+                type a,
+              }
+            `,
+            code: dedent`
+              export {
+                type a,
+                type cFoo,
+                foo,
+              }
+            `,
+          },
+        ],
+        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`
+                export {
+                  iHaveFooInMyName,
+                  meTooIHaveFoo,
+                  a,
+                  b,
+                }
+              `,
+            },
+          ],
+          invalid: [],
+        },
+      )
+    })
+
+    describe(`${ruleName}: newlinesBetween`, () => {
+      ruleTester.run(
+        `${ruleName}(${type}): removes newlines when never`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'y',
+                    left: 'a',
+                  },
+                  messageId: 'extraSpacingBetweenNamedExports',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedNamedExportsOrder',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'z',
+                  },
+                  messageId: 'extraSpacingBetweenNamedExports',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    {
+                      elementNamePattern: 'a',
+                      groupName: 'a',
+                    },
+                  ],
+                  groups: ['a', 'unknown'],
+                  newlinesBetween: 'never',
+                },
+              ],
+              code: dedent`
+                export {
+                    a,
+
+
+                   y,
+                  z,
+
+                      b,
+                }
+              `,
+              output: dedent`
+                export {
+                    a,
+                   b,
+                  y,
+                      z,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): keeps one newline when always`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'z',
+                    left: 'a',
+                  },
+                  messageId: 'extraSpacingBetweenNamedExports',
+                },
+                {
+                  data: {
+                    right: 'y',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedNamedExportsOrder',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'y',
+                  },
+                  messageId: 'missedSpacingBetweenNamedExports',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    {
+                      elementNamePattern: 'a',
+                      groupName: 'a',
+                    },
+                    {
+                      elementNamePattern: 'b',
+                      groupName: 'b',
+                    },
+                  ],
+                  groups: ['a', 'unknown', 'b'],
+                  newlinesBetween: 'always',
+                },
+              ],
+              output: dedent`
+                export {
+                    a,
+
+                   y,
+                  z,
+
+                      b,
+                }
+              `,
+              code: dedent`
+                export {
+                    a,
+
+
+                   z,
+                  y,
+                      b,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      describe(`${ruleName}(${type}): "newlinesBetween" inside groups`, () => {
+        ruleTester.run(
+          `${ruleName}(${type}): handles "newlinesBetween" between consecutive groups`,
+          rule,
+          {
+            invalid: [
+              {
+                options: [
+                  {
+                    ...options,
+                    groups: [
+                      'a',
+                      { newlinesBetween: 'always' },
+                      'b',
+                      { newlinesBetween: 'always' },
+                      'c',
+                      { newlinesBetween: 'never' },
+                      'd',
+                      { newlinesBetween: 'ignore' },
+                      'e',
+                    ],
+                    customGroups: [
+                      { elementNamePattern: 'a', groupName: 'a' },
+                      { elementNamePattern: 'b', groupName: 'b' },
+                      { elementNamePattern: 'c', groupName: 'c' },
+                      { elementNamePattern: 'd', groupName: 'd' },
+                      { elementNamePattern: 'e', groupName: 'e' },
+                    ],
+                    newlinesBetween: 'always',
+                  },
+                ],
+                errors: [
+                  {
+                    data: {
+                      right: 'b',
+                      left: 'a',
+                    },
+                    messageId: 'missedSpacingBetweenNamedExports',
+                  },
+                  {
+                    data: {
+                      right: 'c',
+                      left: 'b',
+                    },
+                    messageId: 'extraSpacingBetweenNamedExports',
+                  },
+                  {
+                    data: {
+                      right: 'd',
+                      left: 'c',
+                    },
+                    messageId: 'extraSpacingBetweenNamedExports',
+                  },
+                ],
+                code: dedent`
+                    export {
+                    a,
+                    b,
+
+
+                    c,
+
+                    d,
+
+
+                    e,
+                  }
+                `,
+                output: dedent`
+                  export {
+                    a,
+
+                    b,
+
+                    c,
+                    d,
+
+
+                    e,
+                  }
+                `,
+              },
+            ],
+            valid: [],
+          },
+        )
+
+        describe(`${ruleName}(${type}): "newlinesBetween" between non-consecutive groups`, () => {
+          for (let [globalNewlinesBetween, groupNewlinesBetween] of [
+            ['always', 'never'] as const,
+            ['always', 'ignore'] as const,
+            ['never', 'always'] as const,
+            ['ignore', 'always'] as const,
+          ]) {
+            ruleTester.run(
+              `${ruleName}(${type}): enforces a newline if the global option is "${globalNewlinesBetween}" and the group option is "${groupNewlinesBetween}"`,
+              rule,
+              {
+                invalid: [
+                  {
+                    options: [
+                      {
+                        ...options,
+                        customGroups: [
+                          { elementNamePattern: 'a', groupName: 'a' },
+                          { elementNamePattern: 'b', groupName: 'b' },
+                          { groupName: 'unusedGroup', elementNamePattern: 'X' },
+                        ],
+                        groups: [
+                          'a',
+                          'unusedGroup',
+                          { newlinesBetween: groupNewlinesBetween },
+                          'b',
+                        ],
+                        newlinesBetween: globalNewlinesBetween,
+                      },
+                    ],
+                    errors: [
+                      {
+                        data: {
+                          right: 'b',
+                          left: 'a',
+                        },
+                        messageId: 'missedSpacingBetweenNamedExports',
+                      },
+                    ],
+                    output: dedent`
+                      export {
+                        a,
+
+                        b,
+                      }
+                    `,
+                    code: dedent`
+                      export {
+                        a,
+                        b,
+                      }
+                    `,
+                  },
+                ],
+                valid: [],
+              },
+            )
+          }
+
+          for (let globalNewlinesBetween of [
+            'always',
+            'ignore',
+            'never',
+          ] as const) {
+            ruleTester.run(
+              `${ruleName}(${type}): enforces no newline if the global option is "${globalNewlinesBetween}" and "newlinesBetween: never" exists between all groups`,
+              rule,
+              {
+                invalid: [
+                  {
+                    options: [
+                      {
+                        ...options,
+                        groups: [
+                          'a',
+                          { newlinesBetween: 'never' },
+                          'unusedGroup',
+                          { newlinesBetween: 'never' },
+                          'b',
+                          { newlinesBetween: 'always' },
+                          'c',
+                        ],
+                        customGroups: [
+                          { elementNamePattern: 'a', groupName: 'a' },
+                          { elementNamePattern: 'b', groupName: 'b' },
+                          { elementNamePattern: 'c', groupName: 'c' },
+                          { groupName: 'unusedGroup', elementNamePattern: 'X' },
+                        ],
+                        newlinesBetween: globalNewlinesBetween,
+                      },
+                    ],
+                    errors: [
+                      {
+                        data: {
+                          right: 'b',
+                          left: 'a',
+                        },
+                        messageId: 'extraSpacingBetweenNamedExports',
+                      },
+                    ],
+                    output: dedent`
+                      export {
+                        a,
+                        b,
+                      }
+                    `,
+                    code: dedent`
+                      export {
+                        a,
+
+                        b,
+                      }
+                    `,
+                  },
+                ],
+                valid: [],
+              },
+            )
+          }
+
+          for (let [globalNewlinesBetween, groupNewlinesBetween] of [
+            ['ignore', 'never'] as const,
+            ['never', 'ignore'] as const,
+          ]) {
+            ruleTester.run(
+              `${ruleName}(${type}): does not enforce a newline if the global option is "${globalNewlinesBetween}" and the group option is "${groupNewlinesBetween}"`,
+              rule,
+              {
+                valid: [
+                  {
+                    options: [
+                      {
+                        ...options,
+                        customGroups: [
+                          { elementNamePattern: 'a', groupName: 'a' },
+                          { elementNamePattern: 'b', groupName: 'b' },
+                          { groupName: 'unusedGroup', elementNamePattern: 'X' },
+                        ],
+                        groups: [
+                          'a',
+                          'unusedGroup',
+                          { newlinesBetween: groupNewlinesBetween },
+                          'b',
+                        ],
+                        newlinesBetween: globalNewlinesBetween,
+                      },
+                    ],
+                    code: dedent`
+                      export {
+                        a,
+
+                        b,
+                      }
+                    `,
+                  },
+                  {
+                    options: [
+                      {
+                        ...options,
+                        customGroups: [
+                          { elementNamePattern: 'a', groupName: 'a' },
+                          { elementNamePattern: 'b', groupName: 'b' },
+                          { groupName: 'unusedGroup', elementNamePattern: 'X' },
+                        ],
+                        groups: [
+                          'a',
+                          'unusedGroup',
+                          { newlinesBetween: groupNewlinesBetween },
+                          'b',
+                        ],
+                        newlinesBetween: globalNewlinesBetween,
+                      },
+                    ],
+                    code: dedent`
+                      export {
+                        a,
+                        b,
+                      }
+                    `,
+                  },
+                ],
+                invalid: [],
+              },
+            )
+          }
+        })
+      })
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  export {
+                    a, // Comment after
+                    b,
+
+                    c,
+                  }
+                `,
+                dedent`
+                  export {
+                    a, // Comment after
+
+                    b,
+                    c,
+                  }
+                `,
+              ],
+              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: 'unexpectedNamedExportsGroupOrder',
+                },
+              ],
+              code: dedent`
+                export {
+                  b,
+                  a, // Comment after
+
+                  c,
+                }
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+    })
   })
 
   describe(`${ruleName}: sorting by natural order`, () => {
@@ -1278,6 +2129,53 @@ describe(ruleName, () => {
       ],
       invalid: [],
     })
+
+    ruleTester.run(`${ruleName}(${type}): enforces newlines between`, rule, {
+      invalid: [
+        {
+          options: [
+            {
+              ...options,
+              customGroups: [
+                {
+                  elementNamePattern: 'a',
+                  groupName: 'a',
+                },
+                {
+                  elementNamePattern: 'b',
+                  groupName: 'b',
+                },
+              ],
+              newlinesBetween: 'always',
+              groups: ['b', 'a'],
+            },
+          ],
+          errors: [
+            {
+              data: {
+                right: 'a',
+                left: 'b',
+              },
+              messageId: 'missedSpacingBetweenNamedExports',
+            },
+          ],
+          output: dedent`
+            export {
+                b,
+
+                a,
+            }
+          `,
+          code: dedent`
+            export {
+                b,
+                a,
+            }
+          `,
+        },
+      ],
+      valid: [],
+    })
   })
 
   describe(`${ruleName}: misc`, () => {
diff --git a/test/rules/sort-named-imports.test.ts b/test/rules/sort-named-imports.test.ts
index 9518bd1..72a7ec7 100644
--- a/test/rules/sort-named-imports.test.ts
+++ b/test/rules/sort-named-imports.test.ts
@@ -1006,6 +1006,857 @@ describe(ruleName, () => {
         valid: [],
       },
     )
+
+    describe(`${ruleName}: custom groups`, () => {
+      ruleTester.run(`${ruleName}: filters on modifier`, rule, {
+        invalid: [
+          {
+            errors: [
+              {
+                data: {
+                  rightGroup: 'typeElements',
+                  leftGroup: 'unknown',
+                  right: 'b',
+                  left: 'a',
+                },
+                messageId: 'unexpectedNamedImportsGroupOrder',
+              },
+            ],
+            options: [
+              {
+                customGroups: [
+                  {
+                    groupName: 'typeElements',
+                    modifiers: ['type'],
+                  },
+                ],
+                groups: ['typeElements', 'unknown'],
+              },
+            ],
+            output: dedent`
+              import {
+                type b,
+                a,
+              } from 'module'
+            `,
+            code: dedent`
+              import {
+                a,
+                type b,
+              } from 'module'
+            `,
+          },
+        ],
+        valid: [],
+      })
+
+      for (let elementNamePattern of [
+        'hello',
+        ['noMatch', 'hello'],
+        { pattern: 'HELLO', flags: 'i' },
+        ['noMatch', { pattern: 'HELLO', flags: 'i' }],
+      ]) {
+        ruleTester.run(`${ruleName}: filters on elementNamePattern`, rule, {
+          invalid: [
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      groupName: 'typesStartingWithHello',
+                      modifiers: ['type'],
+                      elementNamePattern,
+                    },
+                  ],
+                  groups: ['typesStartingWithHello', 'unknown'],
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'typesStartingWithHello',
+                    leftGroup: 'unknown',
+                    right: 'helloType',
+                    left: 'b',
+                  },
+                  messageId: 'unexpectedNamedImportsGroupOrder',
+                },
+              ],
+              output: dedent`
+                import {
+                  type helloType,
+                  a,
+                  b,
+                } from 'module'
+              `,
+              code: dedent`
+                import {
+                  a,
+                  b,
+                  type helloType,
+                } from 'module'
+              `,
+            },
+          ],
+          valid: [],
+        })
+      }
+
+      ruleTester.run(
+        `${ruleName}: sort custom groups by overriding 'type' and 'order'`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'bb',
+                    left: 'a',
+                  },
+                  messageId: 'unexpectedNamedImportsOrder',
+                },
+                {
+                  data: {
+                    right: 'ccc',
+                    left: 'bb',
+                  },
+                  messageId: 'unexpectedNamedImportsOrder',
+                },
+                {
+                  data: {
+                    right: 'dddd',
+                    left: 'ccc',
+                  },
+                  messageId: 'unexpectedNamedImportsOrder',
+                },
+                {
+                  data: {
+                    rightGroup: 'reversedTypesByLineLength',
+                    leftGroup: 'unknown',
+                    right: 'eee',
+                    left: 'm',
+                  },
+                  messageId: 'unexpectedNamedImportsGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  customGroups: [
+                    {
+                      groupName: 'reversedTypesByLineLength',
+                      modifiers: ['type'],
+                      type: 'line-length',
+                      order: 'desc',
+                    },
+                  ],
+                  groups: ['reversedTypesByLineLength', 'unknown'],
+                  type: 'alphabetical',
+                  order: 'asc',
+                },
+              ],
+              output: dedent`
+                import {
+                  type dddd,
+                  type ccc,
+                  type eee,
+                  type bb,
+                  type ff,
+                  type a,
+                  type g,
+                  m,
+                  o,
+                  p,
+                } from 'module'
+              `,
+              code: dedent`
+                import {
+                  type a,
+                  type bb,
+                  type ccc,
+                  type dddd,
+                  m,
+                  type eee,
+                  type ff,
+                  type g,
+                  o,
+                  p,
+                } from 'module'
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}: sort custom groups by overriding 'fallbackSort'`,
+        rule,
+        {
+          invalid: [
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      fallbackSort: {
+                        type: 'alphabetical',
+                        order: 'asc',
+                      },
+                      elementNamePattern: '^foo',
+                      type: 'line-length',
+                      groupName: 'foo',
+                      order: 'desc',
+                    },
+                  ],
+                  type: 'alphabetical',
+                  groups: ['foo'],
+                  order: 'asc',
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    right: 'fooBar',
+                    left: 'fooZar',
+                  },
+                  messageId: 'unexpectedNamedImportsOrder',
+                },
+              ],
+              output: dedent`
+                import {
+                  fooBar,
+                  fooZar,
+                } from 'module'
+              `,
+              code: dedent`
+                import {
+                  fooZar,
+                  fooBar,
+                } from 'module'
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}: does not sort custom groups with 'unsorted' type`,
+        rule,
+        {
+          invalid: [
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      groupName: 'unsortedTypes',
+                      modifiers: ['type'],
+                      type: 'unsorted',
+                    },
+                  ],
+                  groups: ['unsortedTypes', 'unknown'],
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'unsortedTypes',
+                    leftGroup: 'unknown',
+                    right: 'c',
+                    left: 'm',
+                  },
+                  messageId: 'unexpectedNamedImportsGroupOrder',
+                },
+              ],
+              output: dedent`
+                import {
+                  type b,
+                  type a,
+                  type d,
+                  type e,
+                  type c,
+                  m,
+                } from 'module'
+              `,
+              code: dedent`
+                import {
+                  type b,
+                  type a,
+                  type d,
+                  type e,
+                  m,
+                  type c,
+                } from 'module'
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(`${ruleName}: sort custom group blocks`, rule, {
+        invalid: [
+          {
+            options: [
+              {
+                customGroups: [
+                  {
+                    anyOf: [
+                      {
+                        elementNamePattern: 'foo|Foo',
+                        modifiers: ['type'],
+                      },
+                      {
+                        elementNamePattern: 'foo|Foo',
+                      },
+                    ],
+                    groupName: 'elementsIncludingFoo',
+                  },
+                ],
+                groups: ['elementsIncludingFoo', 'unknown'],
+              },
+            ],
+            errors: [
+              {
+                data: {
+                  rightGroup: 'elementsIncludingFoo',
+                  leftGroup: 'unknown',
+                  right: 'cFoo',
+                  left: 'a',
+                },
+                messageId: 'unexpectedNamedImportsGroupOrder',
+              },
+            ],
+            output: dedent`
+              import {
+                type cFoo,
+                foo,
+                type a,
+              } from 'module'
+            `,
+            code: dedent`
+              import {
+                type a,
+                type cFoo,
+                foo,
+              } from 'module'
+            `,
+          },
+        ],
+        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`
+                import {
+                  iHaveFooInMyName,
+                  meTooIHaveFoo,
+                  a,
+                  b,
+                } from 'module'
+              `,
+            },
+          ],
+          invalid: [],
+        },
+      )
+    })
+
+    describe(`${ruleName}: newlinesBetween`, () => {
+      ruleTester.run(
+        `${ruleName}(${type}): removes newlines when never`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'y',
+                    left: 'a',
+                  },
+                  messageId: 'extraSpacingBetweenNamedImports',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedNamedImportsOrder',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'z',
+                  },
+                  messageId: 'extraSpacingBetweenNamedImports',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    {
+                      elementNamePattern: 'a',
+                      groupName: 'a',
+                    },
+                  ],
+                  groups: ['a', 'unknown'],
+                  newlinesBetween: 'never',
+                },
+              ],
+              code: dedent`
+                import {
+                    a,
+
+
+                   y,
+                  z,
+
+                      b,
+                } from 'module'
+              `,
+              output: dedent`
+                import {
+                    a,
+                   b,
+                  y,
+                      z,
+                } from 'module'
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): keeps one newline when always`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'z',
+                    left: 'a',
+                  },
+                  messageId: 'extraSpacingBetweenNamedImports',
+                },
+                {
+                  data: {
+                    right: 'y',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedNamedImportsOrder',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'y',
+                  },
+                  messageId: 'missedSpacingBetweenNamedImports',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    {
+                      elementNamePattern: 'a',
+                      groupName: 'a',
+                    },
+                    {
+                      elementNamePattern: 'b',
+                      groupName: 'b',
+                    },
+                  ],
+                  groups: ['a', 'unknown', 'b'],
+                  newlinesBetween: 'always',
+                },
+              ],
+              output: dedent`
+                import {
+                    a,
+
+                   y,
+                  z,
+
+                      b,
+                } from 'module'
+              `,
+              code: dedent`
+                import {
+                    a,
+
+
+                   z,
+                  y,
+                      b,
+                } from 'module'
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      describe(`${ruleName}(${type}): "newlinesBetween" inside groups`, () => {
+        ruleTester.run(
+          `${ruleName}(${type}): handles "newlinesBetween" between consecutive groups`,
+          rule,
+          {
+            invalid: [
+              {
+                options: [
+                  {
+                    ...options,
+                    groups: [
+                      'a',
+                      { newlinesBetween: 'always' },
+                      'b',
+                      { newlinesBetween: 'always' },
+                      'c',
+                      { newlinesBetween: 'never' },
+                      'd',
+                      { newlinesBetween: 'ignore' },
+                      'e',
+                    ],
+                    customGroups: [
+                      { elementNamePattern: 'a', groupName: 'a' },
+                      { elementNamePattern: 'b', groupName: 'b' },
+                      { elementNamePattern: 'c', groupName: 'c' },
+                      { elementNamePattern: 'd', groupName: 'd' },
+                      { elementNamePattern: 'e', groupName: 'e' },
+                    ],
+                    newlinesBetween: 'always',
+                  },
+                ],
+                errors: [
+                  {
+                    data: {
+                      right: 'b',
+                      left: 'a',
+                    },
+                    messageId: 'missedSpacingBetweenNamedImports',
+                  },
+                  {
+                    data: {
+                      right: 'c',
+                      left: 'b',
+                    },
+                    messageId: 'extraSpacingBetweenNamedImports',
+                  },
+                  {
+                    data: {
+                      right: 'd',
+                      left: 'c',
+                    },
+                    messageId: 'extraSpacingBetweenNamedImports',
+                  },
+                ],
+                code: dedent`
+                    import {
+                    a,
+                    b,
+
+
+                    c,
+
+                    d,
+
+
+                    e,
+                  } from 'module'
+                `,
+                output: dedent`
+                  import {
+                    a,
+
+                    b,
+
+                    c,
+                    d,
+
+
+                    e,
+                  } from 'module'
+                `,
+              },
+            ],
+            valid: [],
+          },
+        )
+
+        describe(`${ruleName}(${type}): "newlinesBetween" between non-consecutive groups`, () => {
+          for (let [globalNewlinesBetween, groupNewlinesBetween] of [
+            ['always', 'never'] as const,
+            ['always', 'ignore'] as const,
+            ['never', 'always'] as const,
+            ['ignore', 'always'] as const,
+          ]) {
+            ruleTester.run(
+              `${ruleName}(${type}): enforces a newline if the global option is "${globalNewlinesBetween}" and the group option is "${groupNewlinesBetween}"`,
+              rule,
+              {
+                invalid: [
+                  {
+                    options: [
+                      {
+                        ...options,
+                        customGroups: [
+                          { elementNamePattern: 'a', groupName: 'a' },
+                          { elementNamePattern: 'b', groupName: 'b' },
+                          { groupName: 'unusedGroup', elementNamePattern: 'X' },
+                        ],
+                        groups: [
+                          'a',
+                          'unusedGroup',
+                          { newlinesBetween: groupNewlinesBetween },
+                          'b',
+                        ],
+                        newlinesBetween: globalNewlinesBetween,
+                      },
+                    ],
+                    errors: [
+                      {
+                        data: {
+                          right: 'b',
+                          left: 'a',
+                        },
+                        messageId: 'missedSpacingBetweenNamedImports',
+                      },
+                    ],
+                    output: dedent`
+                      import {
+                        a,
+
+                        b,
+                      } from 'module'
+                    `,
+                    code: dedent`
+                      import {
+                        a,
+                        b,
+                      } from 'module'
+                    `,
+                  },
+                ],
+                valid: [],
+              },
+            )
+          }
+
+          for (let globalNewlinesBetween of [
+            'always',
+            'ignore',
+            'never',
+          ] as const) {
+            ruleTester.run(
+              `${ruleName}(${type}): enforces no newline if the global option is "${globalNewlinesBetween}" and "newlinesBetween: never" exists between all groups`,
+              rule,
+              {
+                invalid: [
+                  {
+                    options: [
+                      {
+                        ...options,
+                        groups: [
+                          'a',
+                          { newlinesBetween: 'never' },
+                          'unusedGroup',
+                          { newlinesBetween: 'never' },
+                          'b',
+                          { newlinesBetween: 'always' },
+                          'c',
+                        ],
+                        customGroups: [
+                          { elementNamePattern: 'a', groupName: 'a' },
+                          { elementNamePattern: 'b', groupName: 'b' },
+                          { elementNamePattern: 'c', groupName: 'c' },
+                          { groupName: 'unusedGroup', elementNamePattern: 'X' },
+                        ],
+                        newlinesBetween: globalNewlinesBetween,
+                      },
+                    ],
+                    errors: [
+                      {
+                        data: {
+                          right: 'b',
+                          left: 'a',
+                        },
+                        messageId: 'extraSpacingBetweenNamedImports',
+                      },
+                    ],
+                    output: dedent`
+                      import {
+                        a,
+                        b,
+                      } from 'module'
+                    `,
+                    code: dedent`
+                      import {
+                        a,
+
+                        b,
+                      } from 'module'
+                    `,
+                  },
+                ],
+                valid: [],
+              },
+            )
+          }
+
+          for (let [globalNewlinesBetween, groupNewlinesBetween] of [
+            ['ignore', 'never'] as const,
+            ['never', 'ignore'] as const,
+          ]) {
+            ruleTester.run(
+              `${ruleName}(${type}): does not enforce a newline if the global option is "${globalNewlinesBetween}" and the group option is "${groupNewlinesBetween}"`,
+              rule,
+              {
+                valid: [
+                  {
+                    options: [
+                      {
+                        ...options,
+                        customGroups: [
+                          { elementNamePattern: 'a', groupName: 'a' },
+                          { elementNamePattern: 'b', groupName: 'b' },
+                          { groupName: 'unusedGroup', elementNamePattern: 'X' },
+                        ],
+                        groups: [
+                          'a',
+                          'unusedGroup',
+                          { newlinesBetween: groupNewlinesBetween },
+                          'b',
+                        ],
+                        newlinesBetween: globalNewlinesBetween,
+                      },
+                    ],
+                    code: dedent`
+                      import {
+                        a,
+
+                        b,
+                      } from 'module'
+                    `,
+                  },
+                  {
+                    options: [
+                      {
+                        ...options,
+                        customGroups: [
+                          { elementNamePattern: 'a', groupName: 'a' },
+                          { elementNamePattern: 'b', groupName: 'b' },
+                          { groupName: 'unusedGroup', elementNamePattern: 'X' },
+                        ],
+                        groups: [
+                          'a',
+                          'unusedGroup',
+                          { newlinesBetween: groupNewlinesBetween },
+                          'b',
+                        ],
+                        newlinesBetween: globalNewlinesBetween,
+                      },
+                    ],
+                    code: dedent`
+                      import {
+                        a,
+                        b,
+                      } from 'module'
+                    `,
+                  },
+                ],
+                invalid: [],
+              },
+            )
+          }
+        })
+      })
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  import {
+                    a, // Comment after
+                    b,
+
+                    c,
+                  } from 'module'
+                `,
+                dedent`
+                  import {
+                    a, // Comment after
+
+                    b,
+                    c,
+                  } from 'module'
+                `,
+              ],
+              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: 'unexpectedNamedImportsGroupOrder',
+                },
+              ],
+              code: dedent`
+                import {
+                  b,
+                  a, // Comment after
+
+                  c,
+                } from 'module'
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+    })
   })
 
   describe(`${ruleName}: sorting by natural order`, () => {
@@ -1822,6 +2673,53 @@ describe(ruleName, () => {
       ],
       invalid: [],
     })
+
+    ruleTester.run(`${ruleName}(${type}): enforces newlines between`, rule, {
+      invalid: [
+        {
+          options: [
+            {
+              ...options,
+              customGroups: [
+                {
+                  elementNamePattern: 'a',
+                  groupName: 'a',
+                },
+                {
+                  elementNamePattern: 'b',
+                  groupName: 'b',
+                },
+              ],
+              newlinesBetween: 'always',
+              groups: ['b', 'a'],
+            },
+          ],
+          errors: [
+            {
+              data: {
+                right: 'a',
+                left: 'b',
+              },
+              messageId: 'missedSpacingBetweenNamedImports',
+            },
+          ],
+          output: dedent`
+            import {
+                b,
+
+                a,
+            } from 'module'
+          `,
+          code: dedent`
+            import {
+                b,
+                a,
+            } from 'module'
+          `,
+        },
+      ],
+      valid: [],
+    })
   })
 
   describe(`${ruleName}: misc`, () => {