Repository: azat-io/eslint-plugin-perfectionist
Pull request #440 (2025-01-09 09:02:32)

feat: adds `newlines-between` to some rules
Most lines added are documentation and tests.

### Description

This PR adds the following:
- `sort-array-includes`: `newlines-between`.
- `sort-sets`: `netwlines-between`.
- `sort-jsx-props`: `netwlines-between` and `partition-by-newlines`.

Bonus:
- Adds missing documentation in other rules.

### What is the purpose of this pull request?

- [x] New Feature



Patch:
diff --git a/docs/content/rules/sort-array-includes.mdx b/docs/content/rules/sort-array-includes.mdx
index dcbf5a3..29e4675 100644
--- a/docs/content/rules/sort-array-includes.mdx
+++ b/docs/content/rules/sort-array-includes.mdx
@@ -232,6 +232,22 @@ if ([
 
 Each group of elements (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`.
+
 ### useConfigurationIf
 
 <sub>
@@ -301,6 +317,25 @@ Predefined groups are characterized by a selector.
 - `literal`: Array elements that are not spread values.
 - `spread`: Array elements that are spread values.
 
+#### 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>
@@ -379,6 +414,7 @@ Custom groups have a higher priority than any predefined group.
                   specialCharacters: 'keep',
                   groupKind: 'literals-first',
                   partitionByNewLine: false,
+                  newlinesBetween: 'ignore',
                   useConfigurationIf: {},
                   groups: [],
                   customGroups: [],
@@ -408,6 +444,7 @@ Custom groups have a higher priority than any predefined group.
                 specialCharacters: 'keep',
                 groupKind: 'literals-first',
                 partitionByNewLine: false,
+                newlinesBetween: 'ignore',
                 useConfigurationIf: {},
                 groups: [],
                 customGroups: [],
diff --git a/docs/content/rules/sort-classes.mdx b/docs/content/rules/sort-classes.mdx
index 7dc0337..7976cec 100644
--- a/docs/content/rules/sort-classes.mdx
+++ b/docs/content/rules/sort-classes.mdx
@@ -266,6 +266,10 @@ Specifies how new lines should be handled between class member groups.
 - `always` — Enforce one new line between each group, and forbid new lines inside a group.
 - `never` — No new lines are allowed in object types.
 
+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`.
 
 ### ignoreCallbackDependenciesPatterns
@@ -556,6 +560,25 @@ abstract class Example extends BaseExample {
 }
 ```
 
+#### Newlines between groups
+
+You may place `newlinesBetween` objects between your groups to enforce the newline behavior between two specific groups.
+
+See the [`newlinesBetween`](#newlinesbetween) option.
+
+This feature is only applicable when `partitionByNewLine` is false.
+
+```ts
+{
+  newlinesBetween: 'always',
+  groups: [
+    'a',
+    { newlinesBetween: 'never' }, // Overrides the global newlinesBetween option
+    'b',
+  ]
+}
+```
+
 ### customGroups
 
 <Important title="Migrating from the old API">
diff --git a/docs/content/rules/sort-interfaces.mdx b/docs/content/rules/sort-interfaces.mdx
index 3448d2c..467dde9 100644
--- a/docs/content/rules/sort-interfaces.mdx
+++ b/docs/content/rules/sort-interfaces.mdx
@@ -248,6 +248,10 @@ Specifies how new lines should be handled between interface groups.
 - `always` — Enforce one new line between each group, and forbid new lines inside a group.
 - `never` — No new lines are allowed between interface members.
 
+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`.
 
 ### [DEPRECATED] groupKind
@@ -433,6 +437,25 @@ interface Interface {
   method(): string
 ```
 
+#### Newlines between groups
+
+You may place `newlinesBetween` objects between your groups to enforce the newline behavior between two specific groups.
+
+See the [`newlinesBetween`](#newlinesbetween) option.
+
+This feature is only applicable when `partitionByNewLine` is false.
+
+```ts
+{
+  newlinesBetween: 'always',
+  groups: [
+    'a',
+    { newlinesBetween: 'never' }, // Overrides the global newlinesBetween option
+    'b',
+  ]
+}
+```
+
 ### customGroups
 
 <Important title="Migrating from the old API">
diff --git a/docs/content/rules/sort-intersection-types.mdx b/docs/content/rules/sort-intersection-types.mdx
index ae214ff..3b518f3 100644
--- a/docs/content/rules/sort-intersection-types.mdx
+++ b/docs/content/rules/sort-intersection-types.mdx
@@ -176,6 +176,10 @@ Specifies how new lines should be handled between intersection type groups.
 - `always` — Enforce one new line between each group, and forbid new lines inside a group.
 - `never` — No new lines are allowed in intersection types.
 
+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
@@ -296,6 +300,25 @@ type Example =
 }
 ```
 
+#### 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',
+  ]
+}
+```
+
 ## Usage
 
 <CodeTabs
diff --git a/docs/content/rules/sort-jsx-props.mdx b/docs/content/rules/sort-jsx-props.mdx
index eb13df0..cae4e24 100644
--- a/docs/content/rules/sort-jsx-props.mdx
+++ b/docs/content/rules/sort-jsx-props.mdx
@@ -200,6 +200,12 @@ Allows you to specify names or patterns for JSX elements that should be ignored
 
 You can specify their names or a regexp pattern to ignore, for example: `'^Table.+'` to ignore all object types whose names begin with the word Table.
 
+### partitionByNewLine
+
+<sub>default: `false`</sub>
+
+When `true`, the rule will not sort members if there is an empty line between them. This can be useful for keeping logically separated groups of members in their defined order.
+
 ### groups
 
 <sub>
@@ -225,6 +231,25 @@ Within a given group, members will be sorted according to the `type`, `order`, `
 Individual groups can be combined together by placing them in an array. The order of groups in that array does not matter.
 All members of the groups in the array will be sorted together as if they were part of a single group.
 
+#### Newlines between groups
+
+You may place `newlinesBetween` objects between your groups to enforce the newline behavior between two specific groups.
+
+See the [`newlinesBetween`](#newlinesbetween) option.
+
+This feature is only applicable when `partitionByNewLine` is false.
+
+```ts
+{
+  newlinesBetween: 'always',
+  groups: [
+    'a',
+    { newlinesBetween: 'never' }, // Overrides the global newlinesBetween option
+    'b',
+  ]
+}
+```
+
 ### customGroups
 
 <sub>
@@ -280,6 +305,8 @@ Custom group matching takes precedence over predefined group matching.
                   ignoreCase: true,
                   specialCharacters: 'keep',
                   ignorePattern: [],
+                  partitionByNewLine: false,
+                  newlinesBetween: 'ignore',
                   groups: [],
                   customGroups: {},
                 },
@@ -307,6 +334,8 @@ Custom group matching takes precedence over predefined group matching.
                 ignoreCase: true,
                 specialCharacters: 'keep',
                 ignorePattern: [],
+                partitionByNewLine: false,
+                newlinesBetween: 'ignore',
                 groups: [],
                 customGroups: {},
               },
diff --git a/docs/content/rules/sort-modules.mdx b/docs/content/rules/sort-modules.mdx
index 7de87d0..4ba1ed5 100644
--- a/docs/content/rules/sort-modules.mdx
+++ b/docs/content/rules/sort-modules.mdx
@@ -290,6 +290,10 @@ Specifies how new lines should be handled between module member groups.
 - `always` — Enforce one new line between each group, and forbid new lines inside a group.
 - `never` — No new lines are allowed in object types.
 
+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
@@ -496,6 +500,24 @@ Example:
  }
 ```
 
+#### 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',
+  ]
+}
+```
 
 ## Usage
 
diff --git a/docs/content/rules/sort-object-types.mdx b/docs/content/rules/sort-object-types.mdx
index 2390df5..549317a 100644
--- a/docs/content/rules/sort-object-types.mdx
+++ b/docs/content/rules/sort-object-types.mdx
@@ -213,6 +213,10 @@ Specifies how new lines should be handled between object type groups.
 - `always` — Enforce one new line between each group, and forbid new lines inside a group.
 - `never` — No new lines are allowed in object types.
 
+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`.
 
 ### [DEPRECATED] groupKind
@@ -529,6 +533,25 @@ type User = {
  }
 ```
 
+#### 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',
+  ]
+}
+```
+
 ## Usage
 
 <CodeTabs
diff --git a/docs/content/rules/sort-sets.mdx b/docs/content/rules/sort-sets.mdx
index 3f72908..5e447df 100644
--- a/docs/content/rules/sort-sets.mdx
+++ b/docs/content/rules/sort-sets.mdx
@@ -235,6 +235,22 @@ let items = new Set([
 
 Each group of elements (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>
@@ -259,6 +275,25 @@ Predefined groups are characterized by a selector.
 - `literal`: Array elements that are not spread values.
 - `spread`: Array elements that are spread values.
 
+#### 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>
@@ -337,6 +372,7 @@ Custom groups have a higher priority than any predefined group.
                   specialCharacters: 'keep',
                   groupKind: 'literals-first',
                   partitionByNewLine: false,
+                  newlinesBetween: 'ignore',
                   useConfigurationIf: {},
                   groups: [],
                   customGroups: [],
@@ -366,6 +402,7 @@ Custom groups have a higher priority than any predefined group.
                 specialCharacters: 'keep',
                 groupKind: 'literals-first',
                 partitionByNewLine: false,
+                newlinesBetween: 'ignore',
                 useConfigurationIf: {},
                 groups: [],
                 customGroups: [],
diff --git a/docs/content/rules/sort-union-types.mdx b/docs/content/rules/sort-union-types.mdx
index 100f406..7c0f682 100644
--- a/docs/content/rules/sort-union-types.mdx
+++ b/docs/content/rules/sort-union-types.mdx
@@ -196,6 +196,10 @@ Specifies how new lines should be handled between union type groups.
 - `always` — Enforce one new line between each group, and forbid new lines inside a group.
 - `never` — No new lines are allowed in union types.
 
+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
@@ -316,6 +320,25 @@ type Example =
 }
 ```
 
+#### 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',
+  ]
+}
+```
+
 ## Usage
 
 <CodeTabs
diff --git a/rules/sort-array-includes.ts b/rules/sort-array-includes.ts
index 7c69e17..88f3211 100644
--- a/rules/sort-array-includes.ts
+++ b/rules/sort-array-includes.ts
@@ -12,6 +12,7 @@ import {
   partitionByCommentJsonSchema,
   partitionByNewLineJsonSchema,
   specialCharactersJsonSchema,
+  newlinesBetweenJsonSchema,
   ignoreCaseJsonSchema,
   buildTypeJsonSchema,
   alphabetJsonSchema,
@@ -19,7 +20,9 @@ import {
   groupsJsonSchema,
   orderJsonSchema,
 } from '../utils/common-json-schemas'
+import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
 import { validateGeneratedGroupsConfiguration } from '../utils/validate-generated-groups-configuration'
+import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration'
 import {
   singleCustomGroupJsonSchema,
   allSelectors,
@@ -34,6 +37,7 @@ import { hasPartitionComment } from '../utils/has-partition-comment'
 import { createNodeIndexMap } from '../utils/create-node-index-map'
 import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
 import { getCommentsBefore } from '../utils/get-comments-before'
+import { getNewlinesErrors } from '../utils/get-newlines-errors'
 import { createEslintRule } from '../utils/create-eslint-rule'
 import { getLinesBetween } from '../utils/get-lines-between'
 import { getGroupNumber } from '../utils/get-group-number'
@@ -52,20 +56,23 @@ import { pairwise } from '../utils/pairwise'
  */
 let cachedGroupsByModifiersAndSelectors = new Map<string, string[]>()
 
+type MESSAGE_ID =
+  | 'missedSpacingBetweenArrayIncludesMembers'
+  | 'extraSpacingBetweenArrayIncludesMembers'
+  | 'unexpectedArrayIncludesGroupOrder'
+  | 'unexpectedArrayIncludesOrder'
+
 interface SortArrayIncludesSortingNode
   extends SortingNode<TSESTree.SpreadElement | TSESTree.Expression> {
   groupKind: 'literal' | 'spread'
 }
 
-type MESSAGE_ID =
-  | 'unexpectedArrayIncludesGroupOrder'
-  | 'unexpectedArrayIncludesOrder'
-
 export let defaultOptions: Required<Options[0]> = {
   groupKind: 'literals-first',
   specialCharacters: 'keep',
   partitionByComment: false,
   partitionByNewLine: false,
+  newlinesBetween: 'ignore',
   useConfigurationIf: {},
   type: 'alphabetical',
   ignoreCase: true,
@@ -96,6 +103,7 @@ export let jsonSchema: JSONSchema4 = {
       type: buildTypeJsonSchema({ withUnsorted: true }),
       partitionByNewLine: partitionByNewLineJsonSchema,
       specialCharacters: specialCharactersJsonSchema,
+      newlinesBetween: newlinesBetweenJsonSchema,
       ignoreCase: ignoreCaseJsonSchema,
       alphabet: alphabetJsonSchema,
       locales: localesJsonSchema,
@@ -124,6 +132,10 @@ export default createEslintRule<Options, MESSAGE_ID>({
             : node.object.arguments
         sortArray<MESSAGE_ID>({
           availableMessageIds: {
+            missedSpacingBetweenMembers:
+              'missedSpacingBetweenArrayIncludesMembers',
+            extraSpacingBetweenMembers:
+              'extraSpacingBetweenArrayIncludesMembers',
             unexpectedGroupOrder: 'unexpectedArrayIncludesGroupOrder',
             unexpectedOrder: 'unexpectedArrayIncludesOrder',
           },
@@ -137,6 +149,10 @@ export default createEslintRule<Options, MESSAGE_ID>({
     messages: {
       unexpectedArrayIncludesGroupOrder:
         'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
+      missedSpacingBetweenArrayIncludesMembers:
+        'Missed spacing between "{{left}}" and "{{right}}" members.',
+      extraSpacingBetweenArrayIncludesMembers:
+        'Extra spacing between "{{left}}" and "{{right}}" members.',
       unexpectedArrayIncludesOrder:
         'Expected "{{right}}" to come before "{{left}}".',
     },
@@ -159,6 +175,8 @@ export let sortArray = <MessageIds extends string>({
   context,
 }: {
   availableMessageIds: {
+    missedSpacingBetweenMembers: MessageIds
+    extraSpacingBetweenMembers: MessageIds
     unexpectedGroupOrder: MessageIds
     unexpectedOrder: MessageIds
   }
@@ -192,12 +210,14 @@ export let sortArray = <MessageIds extends string>({
     ...completeOptions,
     type,
   }
+  validateCustomSortConfiguration(options)
   validateGeneratedGroupsConfiguration({
     customGroups: options.customGroups,
     selectors: allSelectors,
     groups: options.groups,
     modifiers: [],
   })
+  validateNewlinesAndPartitionConfiguration(options)
 
   let eslintDisabledLines = getEslintDisabledLines({
     ruleName: context.id,
@@ -329,34 +349,54 @@ export let sortArray = <MessageIds extends string>({
 
       let indexOfRightExcludingEslintDisabled =
         sortedNodesExcludingEslintDisabled.indexOf(right)
+
+      let messageIds: MessageIds[] = []
+
       if (
-        leftIndex < rightIndex &&
-        leftIndex < indexOfRightExcludingEslintDisabled
+        leftIndex > rightIndex ||
+        leftIndex >= indexOfRightExcludingEslintDisabled
       ) {
-        return
-      }
-
-      context.report({
-        fix: fixer =>
-          makeFixes({
-            sortedNodes: sortedNodesExcludingEslintDisabled,
-            sourceCode,
-            options,
-            fixer,
-            nodes,
-          }),
-        data: {
-          right: toSingleLine(right.name),
-          left: toSingleLine(left.name),
-          rightGroup: right.group,
-          leftGroup: left.group,
-        },
-        messageId:
+        messageIds.push(
           leftNumber === rightNumber
             ? availableMessageIds.unexpectedOrder
             : availableMessageIds.unexpectedGroupOrder,
-        node: right.node,
-      })
+        )
+      }
+
+      messageIds = [
+        ...messageIds,
+        ...getNewlinesErrors({
+          missedSpacingError: availableMessageIds.missedSpacingBetweenMembers,
+          extraSpacingError: availableMessageIds.extraSpacingBetweenMembers,
+          rightNum: rightNumber,
+          leftNum: leftNumber,
+          sourceCode,
+          options,
+          right,
+          left,
+        }),
+      ]
+
+      for (let messageId of messageIds) {
+        context.report({
+          fix: fixer =>
+            makeFixes({
+              sortedNodes: sortedNodesExcludingEslintDisabled,
+              sourceCode,
+              options,
+              fixer,
+              nodes,
+            }),
+          data: {
+            right: toSingleLine(right.name),
+            left: toSingleLine(left.name),
+            rightGroup: right.group,
+            leftGroup: left.group,
+          },
+          node: right.node,
+          messageId,
+        })
+      }
     })
   }
 }
diff --git a/rules/sort-array-includes/types.ts b/rules/sort-array-includes/types.ts
index 107827c..22a33ec 100644
--- a/rules/sort-array-includes/types.ts
+++ b/rules/sort-array-includes/types.ts
@@ -14,6 +14,11 @@ export type Options = Partial<{
     | string[]
     | boolean
     | string
+  groups: (
+    | { newlinesBetween: 'ignore' | 'always' | 'never' }
+    | Group[]
+    | Group
+  )[]
   type: 'alphabetical' | 'line-length' | 'unsorted' | 'natural' | 'custom'
   useConfigurationIf: {
     allNamesMatchPattern?: string
@@ -22,11 +27,11 @@ export type Options = Partial<{
    * @deprecated for {@link `groups`}
    */
   groupKind: 'literals-first' | 'spreads-first' | 'mixed'
+  newlinesBetween: 'ignore' | 'always' | 'never'
   specialCharacters: 'remove' | 'trim' | 'keep'
   locales: NonNullable<Intl.LocalesArgument>
   customGroups: CustomGroup[]
   partitionByNewLine: boolean
-  groups: (Group[] | Group)[]
   order: 'desc' | 'asc'
   ignoreCase: boolean
   alphabet: string
diff --git a/rules/sort-interfaces.ts b/rules/sort-interfaces.ts
index 1aba4c4..c7f5b24 100644
--- a/rules/sort-interfaces.ts
+++ b/rules/sort-interfaces.ts
@@ -34,9 +34,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
       unexpectedInterfacePropertiesGroupOrder:
         'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
       missedSpacingBetweenInterfaceMembers:
-        'Missed spacing between "{{left}}" and "{{right}}" interfaces.',
+        'Missed spacing between "{{left}}" and "{{right}}" properties.',
       extraSpacingBetweenInterfaceMembers:
-        'Extra spacing between "{{left}}" and "{{right}}" interfaces.',
+        'Extra spacing between "{{left}}" and "{{right}}" properties.',
       unexpectedInterfacePropertiesOrder:
         'Expected "{{right}}" to come before "{{left}}".',
     },
diff --git a/rules/sort-jsx-props.ts b/rules/sort-jsx-props.ts
index 8782c10..8e92e88 100644
--- a/rules/sort-jsx-props.ts
+++ b/rules/sort-jsx-props.ts
@@ -3,7 +3,9 @@ import type { TSESTree } from '@typescript-eslint/types'
 import type { SortingNode } from '../types/sorting-node'
 
 import {
+  partitionByNewLineJsonSchema,
   specialCharactersJsonSchema,
+  newlinesBetweenJsonSchema,
   customGroupsJsonSchema,
   ignoreCaseJsonSchema,
   buildTypeJsonSchema,
@@ -12,13 +14,16 @@ import {
   groupsJsonSchema,
   orderJsonSchema,
 } from '../utils/common-json-schemas'
+import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration'
 import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration'
 import { validateGroupsConfiguration } from '../utils/validate-groups-configuration'
 import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
 import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
 import { createNodeIndexMap } from '../utils/create-node-index-map'
 import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
+import { getNewlinesErrors } from '../utils/get-newlines-errors'
 import { createEslintRule } from '../utils/create-eslint-rule'
+import { getLinesBetween } from '../utils/get-lines-between'
 import { getGroupNumber } from '../utils/get-group-number'
 import { getSourceCode } from '../utils/get-source-code'
 import { rangeToDiff } from '../utils/range-to-diff'
@@ -32,11 +37,17 @@ import { matches } from '../utils/matches'
 
 type Options<T extends string[]> = [
   Partial<{
+    groups: (
+      | { newlinesBetween: 'ignore' | 'always' | 'never' }
+      | Group<T>[]
+      | Group<T>
+    )[]
     type: 'alphabetical' | 'line-length' | 'natural' | 'custom'
     customGroups: Record<T[number], string[] | string>
+    newlinesBetween: 'ignore' | 'always' | 'never'
     specialCharacters: 'remove' | 'trim' | 'keep'
     locales: NonNullable<Intl.LocalesArgument>
-    groups: (Group<T>[] | Group<T>)[]
+    partitionByNewLine: boolean
     ignorePattern: string[]
     order: 'desc' | 'asc'
     ignoreCase: boolean
@@ -44,16 +55,22 @@ type Options<T extends string[]> = [
   }>,
 ]
 
+type MESSAGE_ID =
+  | 'missedSpacingBetweenJSXPropsMembers'
+  | 'extraSpacingBetweenJSXPropsMembers'
+  | 'unexpectedJSXPropsGroupOrder'
+  | 'unexpectedJSXPropsOrder'
+
 type Group<T extends string[]> =
   | 'multiline'
   | 'shorthand'
   | 'unknown'
   | T[number]
 
-type MESSAGE_ID = 'unexpectedJSXPropsGroupOrder' | 'unexpectedJSXPropsOrder'
-
 let defaultOptions: Required<Options<string[]>[0]> = {
   specialCharacters: 'keep',
+  newlinesBetween: 'ignore',
+  partitionByNewLine: false,
   type: 'alphabetical',
   ignorePattern: [],
   ignoreCase: true,
@@ -79,6 +96,7 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
         ['multiline', 'shorthand', 'unknown'],
         Object.keys(options.customGroups),
       )
+      validateNewlinesAndPartitionConfiguration(options)
 
       let sourceCode = getSourceCode(context)
 
@@ -98,50 +116,60 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
         sourceCode,
       })
 
-      let parts: SortingNode[][] = node.openingElement.attributes.reduce(
-        (
-          accumulator: SortingNode[][],
-          attribute: TSESTree.JSXSpreadAttribute | TSESTree.JSXAttribute,
-        ) => {
-          if (attribute.type === 'JSXSpreadAttribute') {
-            accumulator.push([])
-            return accumulator
-          }
+      let formattedMembers: SortingNode[][] =
+        node.openingElement.attributes.reduce(
+          (
+            accumulator: SortingNode[][],
+            attribute: TSESTree.JSXSpreadAttribute | TSESTree.JSXAttribute,
+          ) => {
+            if (attribute.type === 'JSXSpreadAttribute') {
+              accumulator.push([])
+              return accumulator
+            }
 
-          let name =
-            attribute.name.type === 'JSXNamespacedName'
-              ? `${attribute.name.namespace.name}:${attribute.name.name.name}`
-              : attribute.name.name
+            let name =
+              attribute.name.type === 'JSXNamespacedName'
+                ? `${attribute.name.namespace.name}:${attribute.name.name.name}`
+                : attribute.name.name
 
-          let { setCustomGroups, defineGroup, getGroup } = useGroups(options)
+            let { setCustomGroups, defineGroup, getGroup } = useGroups(options)
 
-          setCustomGroups(options.customGroups, name)
+            setCustomGroups(options.customGroups, name)
 
-          if (attribute.value === null) {
-            defineGroup('shorthand')
-          } else if (attribute.loc.start.line !== attribute.loc.end.line) {
-            defineGroup('multiline')
-          }
+            if (attribute.value === null) {
+              defineGroup('shorthand')
+            } else if (attribute.loc.start.line !== attribute.loc.end.line) {
+              defineGroup('multiline')
+            }
 
-          let jsxNode: SortingNode = {
-            isEslintDisabled: isNodeEslintDisabled(
-              attribute,
-              eslintDisabledLines,
-            ),
-            size: rangeToDiff(attribute, sourceCode),
-            group: getGroup(),
-            node: attribute,
-            name,
-          }
+            let sortingNode: SortingNode = {
+              isEslintDisabled: isNodeEslintDisabled(
+                attribute,
+                eslintDisabledLines,
+              ),
+              size: rangeToDiff(attribute, sourceCode),
+              group: getGroup(),
+              node: attribute,
+              name,
+            }
 
-          accumulator.at(-1)!.push(jsxNode)
+            let lastSortingNode = accumulator.at(-1)?.at(-1)
+            if (
+              options.partitionByNewLine &&
+              lastSortingNode &&
+              getLinesBetween(sourceCode, lastSortingNode, sortingNode)
+            ) {
+              accumulator.push([])
+            }
 
-          return accumulator
-        },
-        [[]],
-      )
+            accumulator.at(-1)!.push(sortingNode)
+
+            return accumulator
+          },
+          [[]],
+        )
 
-      for (let nodes of parts) {
+      for (let nodes of formattedMembers) {
         let sortNodesExcludingEslintDisabled = (
           ignoreEslintDisabledNodes: boolean,
         ): SortingNode[] =>
@@ -158,35 +186,57 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
 
           let indexOfRightExcludingEslintDisabled =
             sortedNodesExcludingEslintDisabled.indexOf(right)
-          if (
-            leftIndex < rightIndex &&
-            leftIndex < indexOfRightExcludingEslintDisabled
-          ) {
-            return
-          }
 
           let leftNumber = getGroupNumber(options.groups, left)
           let rightNumber = getGroupNumber(options.groups, right)
-          context.report({
-            fix: fixer =>
-              makeFixes({
-                sortedNodes: sortedNodesExcludingEslintDisabled,
-                sourceCode,
-                fixer,
-                nodes,
-              }),
-            data: {
-              rightGroup: right.group,
-              leftGroup: left.group,
-              right: right.name,
-              left: left.name,
-            },
-            messageId:
+
+          let messageIds: MESSAGE_ID[] = []
+
+          if (
+            leftIndex > rightIndex ||
+            leftIndex >= indexOfRightExcludingEslintDisabled
+          ) {
+            messageIds.push(
               leftNumber === rightNumber
                 ? 'unexpectedJSXPropsOrder'
                 : 'unexpectedJSXPropsGroupOrder',
-            node: right.node,
-          })
+            )
+          }
+
+          messageIds = [
+            ...messageIds,
+            ...getNewlinesErrors({
+              missedSpacingError: 'missedSpacingBetweenJSXPropsMembers',
+              extraSpacingError: 'extraSpacingBetweenJSXPropsMembers',
+              rightNum: rightNumber,
+              leftNum: leftNumber,
+              sourceCode,
+              options,
+              right,
+              left,
+            }),
+          ]
+
+          for (let messageId of messageIds) {
+            context.report({
+              fix: fixer =>
+                makeFixes({
+                  sortedNodes: sortedNodesExcludingEslintDisabled,
+                  sourceCode,
+                  options,
+                  fixer,
+                  nodes,
+                }),
+              data: {
+                rightGroup: right.group,
+                leftGroup: left.group,
+                right: right.name,
+                left: left.name,
+              },
+              node: right.node,
+              messageId,
+            })
+          }
         })
       }
     },
@@ -203,7 +253,9 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
             },
             type: 'array',
           },
+          partitionByNewLine: partitionByNewLineJsonSchema,
           specialCharacters: specialCharactersJsonSchema,
+          newlinesBetween: newlinesBetweenJsonSchema,
           customGroups: customGroupsJsonSchema,
           ignoreCase: ignoreCaseJsonSchema,
           alphabet: alphabetJsonSchema,
@@ -219,6 +271,10 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
     messages: {
       unexpectedJSXPropsGroupOrder:
         'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
+      missedSpacingBetweenJSXPropsMembers:
+        'Missed spacing between "{{left}}" and "{{right}}" props.',
+      extraSpacingBetweenJSXPropsMembers:
+        'Extra spacing between "{{left}}" and "{{right}}" props.',
       unexpectedJSXPropsOrder:
         'Expected "{{right}}" to come before "{{left}}".',
     },
diff --git a/rules/sort-object-types.ts b/rules/sort-object-types.ts
index e6567b5..81f57b6 100644
--- a/rules/sort-object-types.ts
+++ b/rules/sort-object-types.ts
@@ -145,9 +145,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
       unexpectedObjectTypesGroupOrder:
         'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
       missedSpacingBetweenObjectTypeMembers:
-        'Missed spacing between "{{left}}" and "{{right}}" types.',
+        'Missed spacing between "{{left}}" and "{{right}}" properties.',
       extraSpacingBetweenObjectTypeMembers:
-        'Extra spacing between "{{left}}" and "{{right}}" types.',
+        'Extra spacing between "{{left}}" and "{{right}}" properties.',
       unexpectedObjectTypesOrder:
         'Expected "{{right}}" to come before "{{left}}".',
     },
diff --git a/rules/sort-sets.ts b/rules/sort-sets.ts
index 531f2e7..2d16975 100644
--- a/rules/sort-sets.ts
+++ b/rules/sort-sets.ts
@@ -3,7 +3,11 @@ import type { Options } from './sort-array-includes/types'
 import { defaultOptions, jsonSchema, sortArray } from './sort-array-includes'
 import { createEslintRule } from '../utils/create-eslint-rule'
 
-type MESSAGE_ID = 'unexpectedSetsGroupOrder' | 'unexpectedSetsOrder'
+type MESSAGE_ID =
+  | 'missedSpacingBetweenSetsMembers'
+  | 'extraSpacingBetweenSetsMembers'
+  | 'unexpectedSetsGroupOrder'
+  | 'unexpectedSetsOrder'
 
 export default createEslintRule<Options, MESSAGE_ID>({
   create: context => ({
@@ -23,6 +27,8 @@ export default createEslintRule<Options, MESSAGE_ID>({
             : node.arguments[0].arguments
         sortArray<MESSAGE_ID>({
           availableMessageIds: {
+            missedSpacingBetweenMembers: 'missedSpacingBetweenSetsMembers',
+            extraSpacingBetweenMembers: 'extraSpacingBetweenSetsMembers',
             unexpectedGroupOrder: 'unexpectedSetsGroupOrder',
             unexpectedOrder: 'unexpectedSetsOrder',
           },
@@ -36,6 +42,10 @@ export default createEslintRule<Options, MESSAGE_ID>({
     messages: {
       unexpectedSetsGroupOrder:
         'Expected "{{right}}" ({{rightGroup}}) to come before "{{left}}" ({{leftGroup}}).',
+      missedSpacingBetweenSetsMembers:
+        'Missed spacing between "{{left}}" and "{{right}}" members.',
+      extraSpacingBetweenSetsMembers:
+        'Extra spacing between "{{left}}" and "{{right}}" members.',
       unexpectedSetsOrder: 'Expected "{{right}}" to come before "{{left}}".',
     },
     docs: {


Test patch:
diff --git a/test/rules/sort-array-includes.test.ts b/test/rules/sort-array-includes.test.ts
index 20f70e5..e80f27f 100644
--- a/test/rules/sort-array-includes.test.ts
+++ b/test/rules/sort-array-includes.test.ts
@@ -1455,6 +1455,294 @@ describe(ruleName, () => {
         },
       )
     })
+
+    describe(`${ruleName}: newlinesBetween`, () => {
+      ruleTester.run(
+        `${ruleName}(${type}): removes newlines when never`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'y',
+                    left: 'a',
+                  },
+                  messageId: 'extraSpacingBetweenArrayIncludesMembers',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedArrayIncludesOrder',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'z',
+                  },
+                  messageId: 'extraSpacingBetweenArrayIncludesMembers',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    {
+                      elementNamePattern: 'a',
+                      groupName: 'a',
+                    },
+                  ],
+                  groups: ['a', 'unknown'],
+                  newlinesBetween: 'never',
+                },
+              ],
+              code: dedent`
+                [
+                  'a',
+
+
+                 'y',
+                'z',
+
+                    'b'
+                ].includes(value)
+              `,
+              output: dedent`
+                [
+                  'a',
+                 'b',
+                'y',
+                    'z'
+                ].includes(value)
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): keeps one newline when always`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'z',
+                    left: 'a',
+                  },
+                  messageId: 'extraSpacingBetweenArrayIncludesMembers',
+                },
+                {
+                  data: {
+                    right: 'y',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedArrayIncludesOrder',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'y',
+                  },
+                  messageId: 'missedSpacingBetweenArrayIncludesMembers',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    {
+                      elementNamePattern: 'a',
+                      groupName: 'a',
+                    },
+                    {
+                      elementNamePattern: 'b',
+                      groupName: 'b',
+                    },
+                  ],
+                  groups: ['a', 'unknown', 'b'],
+                  newlinesBetween: 'always',
+                },
+              ],
+              output: dedent`
+                [
+                  'a',
+
+                 'y',
+                'z',
+
+                    'b',
+                ].includes(value)
+                `,
+              code: dedent`
+                [
+                  'a',
+
+
+                 'z',
+                'y',
+                    'b',
+                ].includes(value)
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`,
+        rule,
+        {
+          invalid: [
+            {
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    { elementNamePattern: 'a', groupName: 'a' },
+                    { elementNamePattern: 'b', groupName: 'b' },
+                    { elementNamePattern: 'c', groupName: 'c' },
+                    { elementNamePattern: 'd', groupName: 'd' },
+                    { elementNamePattern: 'e', groupName: 'e' },
+                  ],
+                  groups: [
+                    'a',
+                    { newlinesBetween: 'always' },
+                    'b',
+                    { newlinesBetween: 'always' },
+                    'c',
+                    { newlinesBetween: 'never' },
+                    'd',
+                    { newlinesBetween: 'ignore' },
+                    'e',
+                  ],
+                  newlinesBetween: 'always',
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    right: 'b',
+                    left: 'a',
+                  },
+                  messageId: 'missedSpacingBetweenArrayIncludesMembers',
+                },
+                {
+                  data: {
+                    right: 'c',
+                    left: 'b',
+                  },
+                  messageId: 'extraSpacingBetweenArrayIncludesMembers',
+                },
+                {
+                  data: {
+                    right: 'd',
+                    left: 'c',
+                  },
+                  messageId: 'extraSpacingBetweenArrayIncludesMembers',
+                },
+              ],
+              output: dedent`
+                [
+                  'a',
+
+                  'b',
+
+                  'c',
+                  'd',
+
+
+                  'e'
+                ].includes(value)
+              `,
+              code: dedent`
+                [
+                  'a',
+                  'b',
+
+
+                  'c',
+
+                  'd',
+
+
+                  'e'
+                ].includes(value)
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  [
+                    'a', // Comment after
+                    'b',
+
+                    'c'
+                  ].includes(value)
+                `,
+                dedent`
+                  [
+                    'a', // Comment after
+
+                    'b',
+                    'c'
+                  ].includes(value)
+                `,
+              ],
+              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: 'unexpectedArrayIncludesGroupOrder',
+                },
+              ],
+              code: dedent`
+                [
+                  'b',
+                  'a', // Comment after
+
+                  'c'
+                ].includes(value)
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+    })
   })
 
   describe(`${ruleName}: sorting by natural order`, () => {
diff --git a/test/rules/sort-jsx-props.test.ts b/test/rules/sort-jsx-props.test.ts
index 917e9d3..f222089 100644
--- a/test/rules/sort-jsx-props.test.ts
+++ b/test/rules/sort-jsx-props.test.ts
@@ -591,6 +591,336 @@ describe(ruleName, () => {
       ],
       invalid: [],
     })
+
+    ruleTester.run(
+      `${ruleName}(${type}): allows to use new line as partition`,
+      rule,
+      {
+        invalid: [
+          {
+            errors: [
+              {
+                data: {
+                  right: 'a',
+                  left: 'd',
+                },
+                messageId: 'unexpectedJSXPropsOrder',
+              },
+              {
+                data: {
+                  right: 'b',
+                  left: 'e',
+                },
+                messageId: 'unexpectedJSXPropsOrder',
+              },
+            ],
+            output: dedent`
+                <Component
+                  a
+                  d
+
+                  c
+
+                  b
+                  e
+                />
+              `,
+            code: dedent`
+                <Component
+                  d
+                  a
+
+                  c
+
+                  e
+                  b
+                />
+              `,
+            options: [
+              {
+                ...options,
+                partitionByNewLine: true,
+              },
+            ],
+          },
+        ],
+        valid: [],
+      },
+    )
+
+    describe(`${ruleName}: newlinesBetween`, () => {
+      ruleTester.run(
+        `${ruleName}(${type}): removes newlines when never`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'y',
+                    left: 'a',
+                  },
+                  messageId: 'extraSpacingBetweenJSXPropsMembers',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedJSXPropsOrder',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'z',
+                  },
+                  messageId: 'extraSpacingBetweenJSXPropsMembers',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: { a: 'a' },
+                  groups: ['a', 'unknown'],
+                  newlinesBetween: 'never',
+                },
+              ],
+              code: dedent`
+                <Component
+                  a
+
+
+                 y
+                z
+
+                    b
+                />
+              `,
+              output: dedent`
+                <Component
+                  a
+                 b
+                y
+                    z
+                />
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): keeps one newline when always`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'z',
+                    left: 'a',
+                  },
+                  messageId: 'extraSpacingBetweenJSXPropsMembers',
+                },
+                {
+                  data: {
+                    right: 'y',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedJSXPropsOrder',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'y',
+                  },
+                  messageId: 'missedSpacingBetweenJSXPropsMembers',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: {
+                    a: 'a',
+                    b: 'b',
+                  },
+                  groups: ['a', 'unknown', 'b'],
+                  newlinesBetween: 'always',
+                },
+              ],
+              output: dedent`
+                <Component
+                  a
+
+                 y
+                z
+
+                    b
+                />
+                `,
+              code: dedent`
+                <Component
+                  a
+
+
+                 z
+                y
+                    b
+                />
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`,
+        rule,
+        {
+          invalid: [
+            {
+              options: [
+                {
+                  ...options,
+                  groups: [
+                    'a',
+                    { newlinesBetween: 'always' },
+                    'b',
+                    { newlinesBetween: 'always' },
+                    'c',
+                    { newlinesBetween: 'never' },
+                    'd',
+                    { newlinesBetween: 'ignore' },
+                    'e',
+                  ],
+                  customGroups: {
+                    a: 'a',
+                    b: 'b',
+                    c: 'c',
+                    d: 'd',
+                    e: 'e',
+                  },
+                  newlinesBetween: 'always',
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    right: 'b',
+                    left: 'a',
+                  },
+                  messageId: 'missedSpacingBetweenJSXPropsMembers',
+                },
+                {
+                  data: {
+                    right: 'c',
+                    left: 'b',
+                  },
+                  messageId: 'extraSpacingBetweenJSXPropsMembers',
+                },
+                {
+                  data: {
+                    right: 'd',
+                    left: 'c',
+                  },
+                  messageId: 'extraSpacingBetweenJSXPropsMembers',
+                },
+              ],
+              output: dedent`
+                <Component
+                  a
+
+                  b
+
+                  c
+                  d
+
+
+                  e
+                />
+              `,
+              code: dedent`
+                <Component
+                  a
+                  b
+
+
+                  c
+
+                  d
+
+
+                  e
+                />
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  <Component
+                    a // Comment after
+                    b
+
+                    c
+                  />
+                `,
+                dedent`
+                  <Component
+                    a // Comment after
+
+                    b
+                    c
+                  />
+                `,
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'unknown',
+                    leftGroup: 'b|c',
+                    right: 'a',
+                    left: 'b',
+                  },
+                  messageId: 'unexpectedJSXPropsGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  customGroups: {
+                    'b|c': 'b|c',
+                  },
+                  groups: ['unknown', 'b|c'],
+                  newlinesBetween: 'always',
+                },
+              ],
+              code: dedent`
+                <Component
+                  b
+                  a // Comment after
+
+                  c
+                />
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+    })
   })
 
   describe(`${ruleName}: sorting by natural order`, () => {
diff --git a/test/rules/sort-sets.test.ts b/test/rules/sort-sets.test.ts
index c315718..8a0cd05 100644
--- a/test/rules/sort-sets.test.ts
+++ b/test/rules/sort-sets.test.ts
@@ -1402,6 +1402,294 @@ describe(ruleName, () => {
         },
       )
     })
+
+    describe(`${ruleName}: newlinesBetween`, () => {
+      ruleTester.run(
+        `${ruleName}(${type}): removes newlines when never`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'y',
+                    left: 'a',
+                  },
+                  messageId: 'extraSpacingBetweenSetsMembers',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedSetsOrder',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'z',
+                  },
+                  messageId: 'extraSpacingBetweenSetsMembers',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    {
+                      elementNamePattern: 'a',
+                      groupName: 'a',
+                    },
+                  ],
+                  groups: ['a', 'unknown'],
+                  newlinesBetween: 'never',
+                },
+              ],
+              code: dedent`
+                new Set([
+                  'a',
+
+
+                 'y',
+                'z',
+
+                    'b'
+                ])
+              `,
+              output: dedent`
+                new Set([
+                  'a',
+                 'b',
+                'y',
+                    'z'
+                ])
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): keeps one newline when always`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    right: 'z',
+                    left: 'a',
+                  },
+                  messageId: 'extraSpacingBetweenSetsMembers',
+                },
+                {
+                  data: {
+                    right: 'y',
+                    left: 'z',
+                  },
+                  messageId: 'unexpectedSetsOrder',
+                },
+                {
+                  data: {
+                    right: 'b',
+                    left: 'y',
+                  },
+                  messageId: 'missedSpacingBetweenSetsMembers',
+                },
+              ],
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    {
+                      elementNamePattern: 'a',
+                      groupName: 'a',
+                    },
+                    {
+                      elementNamePattern: 'b',
+                      groupName: 'b',
+                    },
+                  ],
+                  groups: ['a', 'unknown', 'b'],
+                  newlinesBetween: 'always',
+                },
+              ],
+              output: dedent`
+                new Set([
+                  'a',
+
+                 'y',
+                'z',
+
+                    'b',
+                ])
+                `,
+              code: dedent`
+                new Set([
+                  'a',
+
+
+                 'z',
+                'y',
+                    'b',
+                ])
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): allows to use "newlinesBetween" inside groups`,
+        rule,
+        {
+          invalid: [
+            {
+              options: [
+                {
+                  ...options,
+                  customGroups: [
+                    { elementNamePattern: 'a', groupName: 'a' },
+                    { elementNamePattern: 'b', groupName: 'b' },
+                    { elementNamePattern: 'c', groupName: 'c' },
+                    { elementNamePattern: 'd', groupName: 'd' },
+                    { elementNamePattern: 'e', groupName: 'e' },
+                  ],
+                  groups: [
+                    'a',
+                    { newlinesBetween: 'always' },
+                    'b',
+                    { newlinesBetween: 'always' },
+                    'c',
+                    { newlinesBetween: 'never' },
+                    'd',
+                    { newlinesBetween: 'ignore' },
+                    'e',
+                  ],
+                  newlinesBetween: 'always',
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    right: 'b',
+                    left: 'a',
+                  },
+                  messageId: 'missedSpacingBetweenSetsMembers',
+                },
+                {
+                  data: {
+                    right: 'c',
+                    left: 'b',
+                  },
+                  messageId: 'extraSpacingBetweenSetsMembers',
+                },
+                {
+                  data: {
+                    right: 'd',
+                    left: 'c',
+                  },
+                  messageId: 'extraSpacingBetweenSetsMembers',
+                },
+              ],
+              output: dedent`
+                new Set([
+                  'a',
+
+                  'b',
+
+                  'c',
+                  'd',
+
+
+                  'e'
+                ])
+              `,
+              code: dedent`
+                new Set([
+                  'a',
+                  'b',
+
+
+                  'c',
+
+                  'd',
+
+
+                  'e'
+                ])
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  new Set([
+                    'a', // Comment after
+                    'b',
+
+                    'c'
+                  ])
+                `,
+                dedent`
+                  new Set([
+                    '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: 'unexpectedSetsGroupOrder',
+                },
+              ],
+              code: dedent`
+                new Set([
+                  'b',
+                  'a', // Comment after
+
+                  'c'
+                ])
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+    })
   })
 
   describe(`${ruleName}: sorting by natural order`, () => {