Repository: azat-io/eslint-plugin-perfectionist
Pull request #441 (2025-01-10 07:29:43)

fix: fixes overlapping errors between `newlinesBetween` and `comment-after` fixes
Related issue: https://github.com/azat-io/eslint-plugin-perfectionist/issues/11
Related comment: https://github.com/azat-io/eslint-plugin-perfectionist/issues/11#issuecomment-2581673008

## Description

Note: good news! I've managed to find a way to fix this without impacting any existing test 🙂

In some specific cases, comments-related fixes and newline fixes may overlap.

### Example

```ts
type Test = {
  onAdd: () => void;
  title: string; // The fix related to this comment will overlap with newline fixes

  onClick: () => void; 
};
```

```ts
'perfectionist/sort-object-types': [
  'error',
  {
    type: 'natural',
    groups: [
      'property',
      'required-method',
    ],
    newlinesBetween: 'always',
  },
]
```

Today, both comment/order fixes and newlines fixes are done in the same cycle. The easiest fix is to separate those fixes into a different cycle (see below).

## Proposal

The PR includes a refactor part and a simple fix that aims to separate the **"comments after"** fixes and the **newlines** fixes. **"comments after"** fixes are fixes related to comments appearing at the end of lines, after nodes.

### State today

- `makeFixes` handles:
  - **Order**-related fixes.
  - **Comments after** fixes.
- `makeNewlinesFixes` handles only newline fixes.

### Commits split

The refactor commits (first 5 commits) rename and split existing functions into new functions in order to separate each type of fixes independently.

This allows us in the end to do

```ts
  let orderFixes = makeOrderFixes({...})

  let commentAfterFixes = makeCommentAfterFixes({...})
  if (commentAfterFixes.length) {
    return [...orderFixes, ...commentAfterFixes]
  }

  let newlinesFixes = makeNewlinesFixes({...})
  return [...orderFixes, ...newlinesFixes]
```

## What is the purpose of this pull request?

- [x] Bug fix


azat-io: Looks good. I am a bit confused by the long function names. And I want to take some time to think if it can be made a bit simpler.
hugop95: @azat-io I've simplified things in the last two commits:
- https://github.com/azat-io/eslint-plugin-perfectionist/pull/441/commits/8732a401a0a86da5956b3c8d1ee8bf6c5db43d0f
- https://github.com/azat-io/eslint-plugin-perfectionist/pull/441/commits/ce18e182552e57b0986be1fe3d5929724f3e80e7

The remaining **fixies** functions are
- `makeAllFixes`
- `makeNewlinesFixes`
- `makeCommentAfterFixes`
- `makeOrderFixes`

Patch:
diff --git a/rules/sort-classes.ts b/rules/sort-classes.ts
index 45b9ce4..d68e698 100644
--- a/rules/sort-classes.ts
+++ b/rules/sort-classes.ts
@@ -42,7 +42,6 @@ import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
 import { hasPartitionComment } from '../utils/has-partition-comment'
 import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
 import { getCommentsBefore } from '../utils/get-comments-before'
-import { makeNewlinesFixes } from '../utils/make-newlines-fixes'
 import { getNewlinesErrors } from '../utils/get-newlines-errors'
 import { createEslintRule } from '../utils/create-eslint-rule'
 import { getLinesBetween } from '../utils/get-lines-between'
@@ -52,8 +51,8 @@ import { toSingleLine } from '../utils/to-single-line'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { useGroups } from '../utils/use-groups'
 import { makeFixes } from '../utils/make-fixes'
+import { useGroups } from '../utils/use-groups'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 import { matches } from '../utils/matches'
@@ -652,22 +651,6 @@ export default createEslintRule<SortClassesOptions, MESSAGE_ID>({
 
         for (let messageId of messageIds) {
           context.report({
-            fix: (fixer: TSESLint.RuleFixer) => [
-              ...makeFixes({
-                sortedNodes: sortedNodesExcludingEslintDisabled,
-                sourceCode,
-                options,
-                fixer,
-                nodes,
-              }),
-              ...makeNewlinesFixes({
-                sortedNodes: sortedNodesExcludingEslintDisabled,
-                sourceCode,
-                options,
-                fixer,
-                nodes,
-              }),
-            ],
             data: {
               nodeDependentOnRight: firstUnorderedNodeDependentOnRight?.name,
               right: toSingleLine(right.name),
@@ -675,6 +658,14 @@ export default createEslintRule<SortClassesOptions, MESSAGE_ID>({
               rightGroup: right.group,
               leftGroup: left.group,
             },
+            fix: (fixer: TSESLint.RuleFixer) =>
+              makeFixes({
+                sortedNodes: sortedNodesExcludingEslintDisabled,
+                sourceCode,
+                options,
+                fixer,
+                nodes,
+              }),
             node: right.node,
             messageId,
           })
diff --git a/rules/sort-decorators.ts b/rules/sort-decorators.ts
index ab2beca..96b0895 100644
--- a/rules/sort-decorators.ts
+++ b/rules/sort-decorators.ts
@@ -31,8 +31,8 @@ import { toSingleLine } from '../utils/to-single-line'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { useGroups } from '../utils/use-groups'
 import { makeFixes } from '../utils/make-fixes'
+import { useGroups } from '../utils/use-groups'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 
diff --git a/rules/sort-enums.ts b/rules/sort-enums.ts
index 9794547..7b97e7c 100644
--- a/rules/sort-enums.ts
+++ b/rules/sort-enums.ts
@@ -31,8 +31,8 @@ import { toSingleLine } from '../utils/to-single-line'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { sortNodes } from '../utils/sort-nodes'
 import { makeFixes } from '../utils/make-fixes'
+import { sortNodes } from '../utils/sort-nodes'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 
diff --git a/rules/sort-exports.ts b/rules/sort-exports.ts
index 3ced890..c952d7c 100644
--- a/rules/sort-exports.ts
+++ b/rules/sort-exports.ts
@@ -23,8 +23,8 @@ import { getLinesBetween } from '../utils/get-lines-between'
 import { getSourceCode } from '../utils/get-source-code'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
-import { sortNodes } from '../utils/sort-nodes'
 import { makeFixes } from '../utils/make-fixes'
+import { sortNodes } from '../utils/sort-nodes'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 
diff --git a/rules/sort-heritage-clauses.ts b/rules/sort-heritage-clauses.ts
index 0ab6200..208c1d6 100644
--- a/rules/sort-heritage-clauses.ts
+++ b/rules/sort-heritage-clauses.ts
@@ -26,8 +26,8 @@ import { toSingleLine } from '../utils/to-single-line'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { useGroups } from '../utils/use-groups'
 import { makeFixes } from '../utils/make-fixes'
+import { useGroups } from '../utils/use-groups'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 
@@ -176,12 +176,6 @@ let sortHeritageClauses = (
     let leftNumber = getGroupNumber(options.groups, left)
     let rightNumber = getGroupNumber(options.groups, right)
     context.report({
-      data: {
-        right: toSingleLine(right.name),
-        left: toSingleLine(left.name),
-        rightGroup: right.group,
-        leftGroup: left.group,
-      },
       fix: fixer =>
         makeFixes({
           sortedNodes: sortedNodesExcludingEslintDisabled,
@@ -189,6 +183,12 @@ let sortHeritageClauses = (
           fixer,
           nodes,
         }),
+      data: {
+        right: toSingleLine(right.name),
+        left: toSingleLine(left.name),
+        rightGroup: right.group,
+        leftGroup: left.group,
+      },
       messageId:
         leftNumber === rightNumber
           ? 'unexpectedHeritageClausesOrder'
diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts
index a9eaf16..c01ef2a 100644
--- a/rules/sort-imports.ts
+++ b/rules/sort-imports.ts
@@ -28,7 +28,6 @@ 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 { makeNewlinesFixes } from '../utils/make-newlines-fixes'
 import { getNewlinesErrors } from '../utils/get-newlines-errors'
 import { createEslintRule } from '../utils/create-eslint-rule'
 import { getLinesBetween } from '../utils/get-lines-between'
@@ -37,8 +36,8 @@ import { getSourceCode } from '../utils/get-source-code'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { useGroups } from '../utils/use-groups'
 import { makeFixes } from '../utils/make-fixes'
+import { useGroups } from '../utils/use-groups'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 import { matches } from '../utils/matches'
@@ -571,15 +570,8 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
 
             for (let messageId of messageIds) {
               context.report({
-                fix: fixer => [
-                  ...makeFixes({
-                    sortedNodes: sortedNodesExcludingEslintDisabled,
-                    nodes: nodeList,
-                    sourceCode,
-                    options,
-                    fixer,
-                  }),
-                  ...makeNewlinesFixes({
+                fix: fixer =>
+                  makeFixes({
                     options: {
                       ...options,
                       customGroups: [],
@@ -589,7 +581,6 @@ export default createEslintRule<Options<string[]>, MESSAGE_ID>({
                     sourceCode,
                     fixer,
                   }),
-                ],
                 data: {
                   rightGroup: right.group,
                   leftGroup: left.group,
diff --git a/rules/sort-jsx-props.ts b/rules/sort-jsx-props.ts
index 63dae54..8782c10 100644
--- a/rules/sort-jsx-props.ts
+++ b/rules/sort-jsx-props.ts
@@ -24,8 +24,8 @@ import { getSourceCode } from '../utils/get-source-code'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { useGroups } from '../utils/use-groups'
 import { makeFixes } from '../utils/make-fixes'
+import { useGroups } from '../utils/use-groups'
 import { pairwise } from '../utils/pairwise'
 import { complete } from '../utils/complete'
 import { matches } from '../utils/matches'
diff --git a/rules/sort-maps.ts b/rules/sort-maps.ts
index d0028ad..79f9719 100644
--- a/rules/sort-maps.ts
+++ b/rules/sort-maps.ts
@@ -25,8 +25,8 @@ import { toSingleLine } from '../utils/to-single-line'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { sortNodes } from '../utils/sort-nodes'
 import { makeFixes } from '../utils/make-fixes'
+import { sortNodes } from '../utils/sort-nodes'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 
diff --git a/rules/sort-modules.ts b/rules/sort-modules.ts
index 9fc3c84..6a13a91 100644
--- a/rules/sort-modules.ts
+++ b/rules/sort-modules.ts
@@ -44,7 +44,6 @@ import { hasPartitionComment } from '../utils/has-partition-comment'
 import { createNodeIndexMap } from '../utils/create-node-index-map'
 import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
 import { getNewlinesErrors } from '../utils/get-newlines-errors'
-import { makeNewlinesFixes } from '../utils/make-newlines-fixes'
 import { getCommentsBefore } from '../utils/get-comments-before'
 import { getNodeDecorators } from '../utils/get-node-decorators'
 import { createEslintRule } from '../utils/create-eslint-rule'
@@ -56,8 +55,8 @@ import { toSingleLine } from '../utils/to-single-line'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { useGroups } from '../utils/use-groups'
 import { makeFixes } from '../utils/make-fixes'
+import { useGroups } from '../utils/use-groups'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 
@@ -442,22 +441,6 @@ let analyzeModule = ({
 
     for (let messageId of messageIds) {
       context.report({
-        fix: (fixer: TSESLint.RuleFixer) => [
-          ...makeFixes({
-            sortedNodes: sortedNodesExcludingEslintDisabled,
-            sourceCode,
-            options,
-            fixer,
-            nodes,
-          }),
-          ...makeNewlinesFixes({
-            sortedNodes: sortedNodesExcludingEslintDisabled,
-            sourceCode,
-            options,
-            fixer,
-            nodes,
-          }),
-        ],
         data: {
           nodeDependentOnRight: firstUnorderedNodeDependentOnRight?.name,
           right: toSingleLine(right.name),
@@ -465,6 +448,14 @@ let analyzeModule = ({
           rightGroup: right.group,
           leftGroup: left.group,
         },
+        fix: (fixer: TSESLint.RuleFixer) =>
+          makeFixes({
+            sortedNodes: sortedNodesExcludingEslintDisabled,
+            sourceCode,
+            options,
+            fixer,
+            nodes,
+          }),
         node: right.node,
         messageId,
       })
diff --git a/rules/sort-named-exports.ts b/rules/sort-named-exports.ts
index 0b400e9..b15c6eb 100644
--- a/rules/sort-named-exports.ts
+++ b/rules/sort-named-exports.ts
@@ -24,8 +24,8 @@ import { getSourceCode } from '../utils/get-source-code'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { sortNodes } from '../utils/sort-nodes'
 import { makeFixes } from '../utils/make-fixes'
+import { sortNodes } from '../utils/sort-nodes'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 
diff --git a/rules/sort-named-imports.ts b/rules/sort-named-imports.ts
index 9076a93..c8ec800 100644
--- a/rules/sort-named-imports.ts
+++ b/rules/sort-named-imports.ts
@@ -24,8 +24,8 @@ import { getSourceCode } from '../utils/get-source-code'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { sortNodes } from '../utils/sort-nodes'
 import { makeFixes } from '../utils/make-fixes'
+import { sortNodes } from '../utils/sort-nodes'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 
diff --git a/rules/sort-object-types.ts b/rules/sort-object-types.ts
index 57139d7..e6567b5 100644
--- a/rules/sort-object-types.ts
+++ b/rules/sort-object-types.ts
@@ -41,7 +41,6 @@ import { isNodeFunctionType } from '../utils/is-node-function-type'
 import { createNodeIndexMap } from '../utils/create-node-index-map'
 import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
 import { getCommentsBefore } from '../utils/get-comments-before'
-import { makeNewlinesFixes } from '../utils/make-newlines-fixes'
 import { getNewlinesErrors } from '../utils/get-newlines-errors'
 import { createEslintRule } from '../utils/create-eslint-rule'
 import { getLinesBetween } from '../utils/get-lines-between'
@@ -447,22 +446,14 @@ export let sortObjectTypeElements = <MessageIds extends string>({
 
       for (let messageId of messageIds) {
         context.report({
-          fix: fixer => [
-            ...makeFixes({
+          fix: fixer =>
+            makeFixes({
               sortedNodes: sortedNodesExcludingEslintDisabled,
               sourceCode,
               options,
               fixer,
               nodes,
             }),
-            ...makeNewlinesFixes({
-              sortedNodes: sortedNodesExcludingEslintDisabled,
-              sourceCode,
-              options,
-              fixer,
-              nodes,
-            }),
-          ],
           data: {
             right: toSingleLine(right.name),
             left: toSingleLine(left.name),
diff --git a/rules/sort-objects.ts b/rules/sort-objects.ts
index 7f31e0a..1445a0b 100644
--- a/rules/sort-objects.ts
+++ b/rules/sort-objects.ts
@@ -39,7 +39,6 @@ import { singleCustomGroupJsonSchema } from './sort-objects/types'
 import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
 import { allModifiers, allSelectors } from './sort-objects/types'
 import { getCommentsBefore } from '../utils/get-comments-before'
-import { makeNewlinesFixes } from '../utils/make-newlines-fixes'
 import { getNewlinesErrors } from '../utils/get-newlines-errors'
 import { createEslintRule } from '../utils/create-eslint-rule'
 import { getLinesBetween } from '../utils/get-lines-between'
@@ -48,8 +47,8 @@ import { getSourceCode } from '../utils/get-source-code'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { useGroups } from '../utils/use-groups'
 import { makeFixes } from '../utils/make-fixes'
+import { useGroups } from '../utils/use-groups'
 import { sortNodes } from '../utils/sort-nodes'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
@@ -502,22 +501,6 @@ export default createEslintRule<Options, MESSAGE_ID>({
 
         for (let messageId of messageIds) {
           context.report({
-            fix: fixer => [
-              ...makeFixes({
-                sortedNodes: sortedNodesExcludingEslintDisabled,
-                sourceCode,
-                options,
-                fixer,
-                nodes,
-              }),
-              ...makeNewlinesFixes({
-                sortedNodes: sortedNodesExcludingEslintDisabled,
-                sourceCode,
-                options,
-                fixer,
-                nodes,
-              }),
-            ],
             data: {
               nodeDependentOnRight: firstUnorderedNodeDependentOnRight?.name,
               rightGroup: right.group,
@@ -525,6 +508,14 @@ export default createEslintRule<Options, MESSAGE_ID>({
               right: right.name,
               left: left.name,
             },
+            fix: fixer =>
+              makeFixes({
+                sortedNodes: sortedNodesExcludingEslintDisabled,
+                sourceCode,
+                options,
+                fixer,
+                nodes,
+              }),
             node: right.node,
             messageId,
           })
diff --git a/rules/sort-switch-case.ts b/rules/sort-switch-case.ts
index 5c3e7e1..d280aaf 100644
--- a/rules/sort-switch-case.ts
+++ b/rules/sort-switch-case.ts
@@ -11,8 +11,8 @@ import {
   localesJsonSchema,
   orderJsonSchema,
 } from '../utils/common-json-schemas'
+import { makeSingleNodeCommentAfterFixes } from '../utils/make-single-node-comment-after-fixes'
 import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration'
-import { makeCommentAfterFixes } from '../utils/make-comment-after-fixes'
 import { createNodeIndexMap } from '../utils/create-node-index-map'
 import { createEslintRule } from '../utils/create-eslint-rule'
 import { getSourceCode } from '../utils/get-source-code'
@@ -182,13 +182,13 @@ export default createEslintRule<Options, MESSAGE_ID>({
                 lastCaseRange,
                 sourceCode.getText(defaultCase.node),
               ),
-              ...makeCommentAfterFixes({
+              ...makeSingleNodeCommentAfterFixes({
                 sortedNode: punctuatorAfterLastCase,
                 node: defaultCase.node,
                 sourceCode,
                 fixer,
               }),
-              ...makeCommentAfterFixes({
+              ...makeSingleNodeCommentAfterFixes({
                 node: punctuatorAfterLastCase,
                 sortedNode: defaultCase.node,
                 sourceCode,
diff --git a/rules/sort-union-types.ts b/rules/sort-union-types.ts
index 492b1f2..58bb45c 100644
--- a/rules/sort-union-types.ts
+++ b/rules/sort-union-types.ts
@@ -25,7 +25,6 @@ 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 { makeNewlinesFixes } from '../utils/make-newlines-fixes'
 import { getNewlinesErrors } from '../utils/get-newlines-errors'
 import { createEslintRule } from '../utils/create-eslint-rule'
 import { getLinesBetween } from '../utils/get-lines-between'
@@ -34,8 +33,8 @@ import { getSourceCode } from '../utils/get-source-code'
 import { toSingleLine } from '../utils/to-single-line'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
-import { useGroups } from '../utils/use-groups'
 import { makeFixes } from '../utils/make-fixes'
+import { useGroups } from '../utils/use-groups'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 
@@ -351,22 +350,14 @@ export let sortUnionOrIntersectionTypes = <MessageIds extends string>({
 
       for (let messageId of messageIds) {
         context.report({
-          fix: fixer => [
-            ...makeFixes({
-              sortedNodes: sortedNodesExcludingEslintDisabled,
-              sourceCode,
-              options,
-              fixer,
-              nodes,
-            }),
-            ...makeNewlinesFixes({
+          fix: fixer =>
+            makeFixes({
               sortedNodes: sortedNodesExcludingEslintDisabled,
               sourceCode,
               options,
               fixer,
               nodes,
             }),
-          ],
           data: {
             right: toSingleLine(right.name),
             left: toSingleLine(left.name),
diff --git a/rules/sort-variable-declarations.ts b/rules/sort-variable-declarations.ts
index 99f803f..90a6fb3 100644
--- a/rules/sort-variable-declarations.ts
+++ b/rules/sort-variable-declarations.ts
@@ -29,8 +29,8 @@ import { toSingleLine } from '../utils/to-single-line'
 import { rangeToDiff } from '../utils/range-to-diff'
 import { getSettings } from '../utils/get-settings'
 import { isSortable } from '../utils/is-sortable'
-import { sortNodes } from '../utils/sort-nodes'
 import { makeFixes } from '../utils/make-fixes'
+import { sortNodes } from '../utils/sort-nodes'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 
diff --git a/utils/get-node-range.ts b/utils/get-node-range.ts
index 78d0c79..fb93980 100644
--- a/utils/get-node-range.ts
+++ b/utils/get-node-range.ts
@@ -9,7 +9,7 @@ import { getCommentsBefore } from './get-comments-before'
 
 interface GetNodeRangeParameters {
   options?: {
-    partitionByComment:
+    partitionByComment?:
       | {
           block?: string[] | boolean | string
           line?: string[] | boolean | string
diff --git a/utils/make-comment-after-fixes.ts b/utils/make-comment-after-fixes.ts
index 26b9212..ee29896 100644
--- a/utils/make-comment-after-fixes.ts
+++ b/utils/make-comment-after-fixes.ts
@@ -1,46 +1,42 @@
 import type { TSESLint } from '@typescript-eslint/utils'
-import type { TSESTree } from '@typescript-eslint/types'
 
-import { getCommentAfter } from './get-comment-after'
+import type { SortingNode } from '../types/sorting-node'
 
-interface CommentAfterFixesParameters {
-  sortedNode: TSESTree.Token | TSESTree.Node
-  node: TSESTree.Token | TSESTree.Node
+import { makeSingleNodeCommentAfterFixes } from './make-single-node-comment-after-fixes'
+
+interface MakeCommentAfterFixesParameters {
   sourceCode: TSESLint.SourceCode
+  sortedNodes: SortingNode[]
   fixer: TSESLint.RuleFixer
+  nodes: SortingNode[]
 }
 
 export let makeCommentAfterFixes = ({
-  sortedNode,
+  sortedNodes,
   sourceCode,
   fixer,
-  node,
-}: CommentAfterFixesParameters): TSESLint.RuleFix[] => {
-  let commentAfter = getCommentAfter(sortedNode, sourceCode)
-  let areNodesOnSameLine = node.loc.start.line === sortedNode.loc.end.line
-  if (!commentAfter || areNodesOnSameLine) {
-    return []
-  }
-
+  nodes,
+}: MakeCommentAfterFixesParameters): TSESLint.RuleFix[] => {
   let fixes: TSESLint.RuleFix[] = []
-  let tokenBefore = sourceCode.getTokenBefore(commentAfter)
-
-  let range: TSESTree.Range = [
-    tokenBefore!.range.at(1)!,
-    commentAfter.range.at(1)!,
-  ]
-
-  fixes.push(fixer.replaceTextRange(range, ''))
-
-  let tokenAfterNode = sourceCode.getTokenAfter(node)
-  fixes.push(
-    fixer.insertTextAfter(
-      tokenAfterNode?.loc.end.line === node.loc.end.line
-        ? tokenAfterNode
-        : node,
-      sourceCode.text.slice(...range),
-    ),
-  )
-
+  for (let max = nodes.length, i = 0; i < max; i++) {
+    let sortingNode = nodes.at(i)!
+    let sortedSortingNode = sortedNodes.at(i)!
+    let { node } = sortingNode
+    let { node: sortedNode } = sortedSortingNode
+
+    if (node === sortedNode) {
+      continue
+    }
+
+    fixes = [
+      ...fixes,
+      ...makeSingleNodeCommentAfterFixes({
+        sortedNode,
+        sourceCode,
+        fixer,
+        node,
+      }),
+    ]
+  }
   return fixes
 }
diff --git a/utils/make-fixes.ts b/utils/make-fixes.ts
index e7d4405..4340a87 100644
--- a/utils/make-fixes.ts
+++ b/utils/make-fixes.ts
@@ -3,11 +3,12 @@ import type { TSESLint } from '@typescript-eslint/utils'
 import type { SortingNode } from '../types/sorting-node'
 
 import { makeCommentAfterFixes } from './make-comment-after-fixes'
-import { getNodeRange } from './get-node-range'
+import { makeNewlinesFixes } from './make-newlines-fixes'
+import { makeOrderFixes } from './make-order-fixes'
 
 interface MakeFixesParameters {
   options?: {
-    partitionByComment:
+    partitionByComment?:
       | {
           block?: string[] | boolean | string
           line?: string[] | boolean | string
@@ -15,6 +16,13 @@ interface MakeFixesParameters {
       | string[]
       | boolean
       | string
+    groups?: (
+      | { newlinesBetween: 'ignore' | 'always' | 'never' }
+      | string[]
+      | string
+    )[]
+    customGroups?: Record<string, string[] | string> | CustomGroup[]
+    newlinesBetween?: 'ignore' | 'always' | 'never'
   }
   ignoreFirstNodeHighestBlockComment?: boolean
   sourceCode: TSESLint.SourceCode
@@ -23,6 +31,11 @@ interface MakeFixesParameters {
   nodes: SortingNode[]
 }
 
+interface CustomGroup {
+  newlinesInside?: 'always' | 'never'
+  groupName: string
+}
+
 export let makeFixes = ({
   ignoreFirstNodeHighestBlockComment,
   sortedNodes,
@@ -31,72 +44,40 @@ export let makeFixes = ({
   fixer,
   nodes,
 }: MakeFixesParameters): TSESLint.RuleFix[] => {
-  let fixes: TSESLint.RuleFix[] = []
+  let orderFixes = makeOrderFixes({
+    ignoreFirstNodeHighestBlockComment,
+    sortedNodes,
+    sourceCode,
+    options,
+    nodes,
+    fixer,
+  })
 
-  for (let max = nodes.length, i = 0; i < max; i++) {
-    let sortingNode = nodes.at(i)!
-    let sortedSortingNode = sortedNodes.at(i)!
-    let { node } = sortingNode
-    let { addSafetySemicolonWhenInline, node: sortedNode } = sortedSortingNode
-    let isNodeFirstNode = node === nodes.at(0)!.node
-    let isSortedNodeFirstNode = sortedNode === nodes.at(0)!.node
-
-    if (node === sortedNode) {
-      continue
-    }
-
-    let sortedNodeCode = sourceCode.text.slice(
-      ...getNodeRange({
-        ignoreHighestBlockComment:
-          ignoreFirstNodeHighestBlockComment && isSortedNodeFirstNode,
-        node: sortedNode,
-        sourceCode,
-        options,
-      }),
-    )
-    let sortedNodeText = sourceCode.getText(sortedNode)
-    let tokensAfter = sourceCode.getTokensAfter(node, {
-      includeComments: false,
-      count: 1,
-    })
-    let nextToken = tokensAfter.at(0)
-
-    let sortedNextNodeEndsWithSafeCharacter =
-      sortedNodeText.endsWith(';') || sortedNodeText.endsWith(',')
-    let isNextTokenOnSameLineAsNode =
-      nextToken?.loc.start.line === node.loc.end.line
-    let isNextTokenSafeCharacter =
-      nextToken?.value === ';' || nextToken?.value === ','
-    if (
-      addSafetySemicolonWhenInline &&
-      isNextTokenOnSameLineAsNode &&
-      !sortedNextNodeEndsWithSafeCharacter &&
-      !isNextTokenSafeCharacter
-    ) {
-      sortedNodeCode += ';'
-    }
-    fixes.push(
-      fixer.replaceTextRange(
-        getNodeRange({
-          ignoreHighestBlockComment:
-            ignoreFirstNodeHighestBlockComment && isNodeFirstNode,
-          sourceCode,
-          options,
-          node,
-        }),
-        sortedNodeCode,
-      ),
-    )
-    fixes = [
-      ...fixes,
-      ...makeCommentAfterFixes({
-        sortedNode,
-        sourceCode,
-        fixer,
-        node,
-      }),
-    ]
+  let commentAfterFixes = makeCommentAfterFixes({
+    sortedNodes,
+    sourceCode,
+    nodes,
+    fixer,
+  })
+  if (
+    commentAfterFixes.length ||
+    !options?.groups ||
+    !options.newlinesBetween
+  ) {
+    return [...orderFixes, ...commentAfterFixes]
   }
 
-  return fixes
+  let newlinesFixes = makeNewlinesFixes({
+    options: {
+      ...options,
+      newlinesBetween: options.newlinesBetween,
+      groups: options.groups,
+    },
+    sortedNodes,
+    sourceCode,
+    fixer,
+    nodes,
+  })
+
+  return [...orderFixes, ...newlinesFixes]
 }
diff --git a/utils/make-order-fixes.ts b/utils/make-order-fixes.ts
new file mode 100644
index 0000000..d956b87
--- /dev/null
+++ b/utils/make-order-fixes.ts
@@ -0,0 +1,92 @@
+import type { TSESLint } from '@typescript-eslint/utils'
+
+import type { SortingNode } from '../types/sorting-node'
+
+import { getNodeRange } from './get-node-range'
+
+interface MakeOrderFixesParameters {
+  options?: {
+    partitionByComment?:
+      | {
+          block?: string[] | boolean | string
+          line?: string[] | boolean | string
+        }
+      | string[]
+      | boolean
+      | string
+  }
+  ignoreFirstNodeHighestBlockComment?: boolean
+  sourceCode: TSESLint.SourceCode
+  sortedNodes: SortingNode[]
+  fixer: TSESLint.RuleFixer
+  nodes: SortingNode[]
+}
+
+export let makeOrderFixes = ({
+  ignoreFirstNodeHighestBlockComment,
+  sortedNodes,
+  sourceCode,
+  options,
+  fixer,
+  nodes,
+}: MakeOrderFixesParameters): TSESLint.RuleFix[] => {
+  let fixes: TSESLint.RuleFix[] = []
+
+  for (let max = nodes.length, i = 0; i < max; i++) {
+    let sortingNode = nodes.at(i)!
+    let sortedSortingNode = sortedNodes.at(i)!
+    let { node } = sortingNode
+    let { addSafetySemicolonWhenInline, node: sortedNode } = sortedSortingNode
+    let isNodeFirstNode = node === nodes.at(0)!.node
+    let isSortedNodeFirstNode = sortedNode === nodes.at(0)!.node
+
+    if (node === sortedNode) {
+      continue
+    }
+
+    let sortedNodeCode = sourceCode.text.slice(
+      ...getNodeRange({
+        ignoreHighestBlockComment:
+          ignoreFirstNodeHighestBlockComment && isSortedNodeFirstNode,
+        node: sortedNode,
+        sourceCode,
+        options,
+      }),
+    )
+    let sortedNodeText = sourceCode.getText(sortedNode)
+    let tokensAfter = sourceCode.getTokensAfter(node, {
+      includeComments: false,
+      count: 1,
+    })
+    let nextToken = tokensAfter.at(0)
+
+    let sortedNextNodeEndsWithSafeCharacter =
+      sortedNodeText.endsWith(';') || sortedNodeText.endsWith(',')
+    let isNextTokenOnSameLineAsNode =
+      nextToken?.loc.start.line === node.loc.end.line
+    let isNextTokenSafeCharacter =
+      nextToken?.value === ';' || nextToken?.value === ','
+    if (
+      addSafetySemicolonWhenInline &&
+      isNextTokenOnSameLineAsNode &&
+      !sortedNextNodeEndsWithSafeCharacter &&
+      !isNextTokenSafeCharacter
+    ) {
+      sortedNodeCode += ';'
+    }
+    fixes.push(
+      fixer.replaceTextRange(
+        getNodeRange({
+          ignoreHighestBlockComment:
+            ignoreFirstNodeHighestBlockComment && isNodeFirstNode,
+          sourceCode,
+          options,
+          node,
+        }),
+        sortedNodeCode,
+      ),
+    )
+  }
+
+  return fixes
+}
diff --git a/utils/make-single-node-comment-after-fixes.ts b/utils/make-single-node-comment-after-fixes.ts
new file mode 100644
index 0000000..3a44959
--- /dev/null
+++ b/utils/make-single-node-comment-after-fixes.ts
@@ -0,0 +1,46 @@
+import type { TSESLint } from '@typescript-eslint/utils'
+import type { TSESTree } from '@typescript-eslint/types'
+
+import { getCommentAfter } from './get-comment-after'
+
+interface MakeSingleNodeCommentAfterFixesParameters {
+  sortedNode: TSESTree.Token | TSESTree.Node
+  node: TSESTree.Token | TSESTree.Node
+  sourceCode: TSESLint.SourceCode
+  fixer: TSESLint.RuleFixer
+}
+
+export let makeSingleNodeCommentAfterFixes = ({
+  sortedNode,
+  sourceCode,
+  fixer,
+  node,
+}: MakeSingleNodeCommentAfterFixesParameters): TSESLint.RuleFix[] => {
+  let commentAfter = getCommentAfter(sortedNode, sourceCode)
+  let areNodesOnSameLine = node.loc.start.line === sortedNode.loc.end.line
+  if (!commentAfter || areNodesOnSameLine) {
+    return []
+  }
+
+  let fixes: TSESLint.RuleFix[] = []
+  let tokenBefore = sourceCode.getTokenBefore(commentAfter)
+
+  let range: TSESTree.Range = [
+    tokenBefore!.range.at(1)!,
+    commentAfter.range.at(1)!,
+  ]
+
+  fixes.push(fixer.replaceTextRange(range, ''))
+
+  let tokenAfterNode = sourceCode.getTokenAfter(node)
+  fixes.push(
+    fixer.insertTextAfter(
+      tokenAfterNode?.loc.end.line === node.loc.end.line
+        ? tokenAfterNode
+        : node,
+      sourceCode.text.slice(...range),
+    ),
+  )
+
+  return fixes
+}


Test patch:
diff --git a/test/rules/sort-classes.test.ts b/test/rules/sort-classes.test.ts
index 387900b..dfecb75 100644
--- a/test/rules/sort-classes.test.ts
+++ b/test/rules/sort-classes.test.ts
@@ -4513,6 +4513,61 @@ describe(ruleName, () => {
           valid: [],
         },
       )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  class Class {
+                    a // Comment after
+                    b() {}
+
+                    c() {}
+                  }
+                `,
+                dedent`
+                  class Class {
+                    a // Comment after
+
+                    b() {}
+                    c() {}
+                  }
+                `,
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'property',
+                    leftGroup: 'method',
+                    right: 'a',
+                    left: 'b',
+                  },
+                  messageId: 'unexpectedClassesGroupOrder',
+                },
+              ],
+              code: dedent`
+                class Class {
+                  b() {}
+                  a // Comment after
+
+                  c() {}
+                }
+              `,
+              options: [
+                {
+                  groups: ['property', 'method'],
+                  newlinesBetween: 'always',
+                },
+              ],
+            },
+          ],
+          valid: [],
+        },
+      )
     })
 
     describe(`${ruleName}(${type}): sorts inline elements correctly`, () => {
diff --git a/test/rules/sort-imports.test.ts b/test/rules/sort-imports.test.ts
index 65c77f8..4eabd2d 100644
--- a/test/rules/sort-imports.test.ts
+++ b/test/rules/sort-imports.test.ts
@@ -2115,6 +2115,55 @@ describe(ruleName, () => {
           valid: [],
         },
       )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  import { a } from './a' // Comment after
+                  import { b } from 'b'
+
+                  import { c } from 'c'
+                `,
+                dedent`
+                  import { a } from './a' // Comment after
+
+                  import { b } from 'b'
+                  import { c } from 'c'
+                `,
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'unknown',
+                    leftGroup: 'external',
+                    right: './a',
+                    left: 'b',
+                  },
+                  messageId: 'unexpectedImportsGroupOrder',
+                },
+              ],
+              code: dedent`
+                  import { b } from 'b'
+                  import { a } from './a' // Comment after
+
+                  import { c } from 'c'
+              `,
+              options: [
+                {
+                  groups: ['unknown', 'external'],
+                  newlinesBetween: 'always',
+                },
+              ],
+            },
+          ],
+          valid: [],
+        },
+      )
     })
 
     ruleTester.run(
diff --git a/test/rules/sort-interfaces.test.ts b/test/rules/sort-interfaces.test.ts
index 363b0cf..2fda71d 100644
--- a/test/rules/sort-interfaces.test.ts
+++ b/test/rules/sort-interfaces.test.ts
@@ -2390,6 +2390,61 @@ describe(ruleName, () => {
           valid: [],
         },
       )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  interface Interface {
+                    a: string // Comment after
+                    b: () => void
+
+                    c: () => void
+                  };
+                `,
+                dedent`
+                  interface Interface {
+                    a: string // Comment after
+
+                    b: () => void
+                    c: () => void
+                  };
+                `,
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'property',
+                    leftGroup: 'method',
+                    right: 'a',
+                    left: 'b',
+                  },
+                  messageId: 'unexpectedInterfacePropertiesGroupOrder',
+                },
+              ],
+              code: dedent`
+                interface Interface {
+                  b: () => void
+                  a: string // Comment after
+
+                  c: () => void
+                };
+              `,
+              options: [
+                {
+                  groups: ['property', 'method'],
+                  newlinesBetween: 'always',
+                },
+              ],
+            },
+          ],
+          valid: [],
+        },
+      )
     })
 
     ruleTester.run(
diff --git a/test/rules/sort-intersection-types.test.ts b/test/rules/sort-intersection-types.test.ts
index 24877ef..66ba6ae 100644
--- a/test/rules/sort-intersection-types.test.ts
+++ b/test/rules/sort-intersection-types.test.ts
@@ -1198,6 +1198,58 @@ describe(ruleName, () => {
           valid: [],
         },
       )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  type T =
+                    & 'a' // Comment after
+                    & B
+
+                    & C
+                `,
+                dedent`
+                  type T =
+                    & 'a' // Comment after
+
+                    & B
+                    & C
+                `,
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'literal',
+                    leftGroup: 'named',
+                    right: "'a'",
+                    left: 'B',
+                  },
+                  messageId: 'unexpectedIntersectionTypesGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  groups: ['literal', 'named'],
+                  newlinesBetween: 'always',
+                },
+              ],
+              code: dedent`
+                type T =
+                  & B
+                  & 'a' // Comment after
+
+                  & C
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
     })
 
     ruleTester.run(
diff --git a/test/rules/sort-modules.test.ts b/test/rules/sort-modules.test.ts
index 237d322..c376e37 100644
--- a/test/rules/sort-modules.test.ts
+++ b/test/rules/sort-modules.test.ts
@@ -2187,6 +2187,55 @@ describe(ruleName, () => {
           valid: [],
         },
       )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  function a() {} // Comment after
+                  type B = string
+
+                  type C = string
+                `,
+                dedent`
+                  function a() {} // Comment after
+
+                  type B = string
+                  type C = string
+                `,
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'function',
+                    leftGroup: 'type',
+                    right: 'a',
+                    left: 'B',
+                  },
+                  messageId: 'unexpectedModulesGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  groups: ['function', 'type'],
+                  newlinesBetween: 'always',
+                },
+              ],
+              code: dedent`
+                type B = string
+                function a() {} // Comment after
+
+                type C = string
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
     })
 
     describe(`${ruleName}(${type}): sorts inline elements correctly`, () => {
diff --git a/test/rules/sort-object-types.test.ts b/test/rules/sort-object-types.test.ts
index 04b6a99..fae3833 100644
--- a/test/rules/sort-object-types.test.ts
+++ b/test/rules/sort-object-types.test.ts
@@ -2186,6 +2186,61 @@ describe(ruleName, () => {
           valid: [],
         },
       )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  type Test = {
+                    a: string // Comment after
+                    b: () => void
+
+                    c: () => void
+                  };
+                `,
+                dedent`
+                  type Test = {
+                    a: string // Comment after
+
+                    b: () => void
+                    c: () => void
+                  };
+                `,
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'property',
+                    leftGroup: 'method',
+                    right: 'a',
+                    left: 'b',
+                  },
+                  messageId: 'unexpectedObjectTypesGroupOrder',
+                },
+              ],
+              code: dedent`
+                type Test = {
+                  b: () => void
+                  a: string // Comment after
+
+                  c: () => void
+                };
+              `,
+              options: [
+                {
+                  groups: ['property', 'method'],
+                  newlinesBetween: 'always',
+                },
+              ],
+            },
+          ],
+          valid: [],
+        },
+      )
     })
 
     ruleTester.run(
diff --git a/test/rules/sort-objects.test.ts b/test/rules/sort-objects.test.ts
index 4f878dd..933c11c 100644
--- a/test/rules/sort-objects.test.ts
+++ b/test/rules/sort-objects.test.ts
@@ -2263,6 +2263,61 @@ describe(ruleName, () => {
           valid: [],
         },
       )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  let obj = {
+                    a, // Comment after
+                    b() {},
+
+                    c() {},
+                  };
+                `,
+                dedent`
+                  let obj = {
+                    a, // Comment after
+
+                    b() {},
+                    c() {},
+                  };
+                `,
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'unknown',
+                    leftGroup: 'method',
+                    right: 'a',
+                    left: 'b',
+                  },
+                  messageId: 'unexpectedObjectsGroupOrder',
+                },
+              ],
+              code: dedent`
+                let obj = {
+                  b() {},
+                  a, // Comment after
+
+                  c() {},
+                };
+              `,
+              options: [
+                {
+                  groups: ['unknown', 'method'],
+                  newlinesBetween: 'always',
+                },
+              ],
+            },
+          ],
+          valid: [],
+        },
+      )
     })
 
     ruleTester.run(
diff --git a/test/rules/sort-union-types.test.ts b/test/rules/sort-union-types.test.ts
index 39cb313..08166f7 100644
--- a/test/rules/sort-union-types.test.ts
+++ b/test/rules/sort-union-types.test.ts
@@ -1201,6 +1201,58 @@ describe(ruleName, () => {
           valid: [],
         },
       )
+
+      ruleTester.run(
+        `${ruleName}(${type}): handles newlines and comment after fixes`,
+        rule,
+        {
+          invalid: [
+            {
+              output: [
+                dedent`
+                  type T =
+                    | 'a' // Comment after
+                    | B
+
+                    | C
+                `,
+                dedent`
+                  type T =
+                    | 'a' // Comment after
+
+                    | B
+                    | C
+                `,
+              ],
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'literal',
+                    leftGroup: 'named',
+                    right: "'a'",
+                    left: 'B',
+                  },
+                  messageId: 'unexpectedUnionTypesGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  groups: ['literal', 'named'],
+                  newlinesBetween: 'always',
+                },
+              ],
+              code: dedent`
+                type T =
+                  | B
+                  | 'a' // Comment after
+
+                  | C
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
     })
 
     ruleTester.run(