Repository: azat-io/eslint-plugin-perfectionist
Pull request #476 (2025-02-22 21:21:47)

feat(sort-objects): add `fallbackSort.sortByValue`-related options
Follow-up of https://github.com/azat-io/eslint-plugin-perfectionist/pull/457, https://github.com/azat-io/eslint-plugin-perfectionist/pull/469 and https://github.com/azat-io/eslint-plugin-perfectionist/pull/474.

Should fully resolve https://github.com/azat-io/eslint-plugin-perfectionist/issues/68 now.

# Description

This final PR adds the following options to `sort-objects` and `sort-interfaces`.
- `fallbackSort.sortByValue`.
- `customGroups.fallbackSort.sortByValue`.

## Example

```json
{
  "groups": [
    "unknown",
    "metaProperties"
  ],
  "customGroups": [
    {
      "type": "line-length",
      "groupName": "metaProperties",
      "elementNamePattern": "^_",
      "fallbackSort": {
        "type": "alphabetical",
        "order": "asc",
        "sortBy": "value"
      }
    }
  ]
}
```

Enforced result:
```ts
interface Interface {
  someNonMetaProperty: any;
  // All meta properties below have the same line-length => Sort them by alphabetical order ASC based on their value
  _arg1: number;
  _arg3: number;
  _arg5: number;
  _arg2: string;
  _arg4: string;
  _arg6: string;
}
```

## What is the purpose of this pull request?

- [x] New Feature


Patch:
diff --git a/docs/content/rules/sort-interfaces.mdx b/docs/content/rules/sort-interfaces.mdx
index 3532e72..6cdb9d0 100644
--- a/docs/content/rules/sort-interfaces.mdx
+++ b/docs/content/rules/sort-interfaces.mdx
@@ -162,6 +162,7 @@ Determines whether the sorted items should be in ascending or descending order.
   {
     type: 'alphabetical' | 'natural' | 'line-length' | 'custom' | 'unsorted'
     order?: 'asc' | 'desc'
+    sortBy?: 'name' | 'value'
   }
   ```
 </sub>
@@ -560,7 +561,7 @@ interface CustomGroupDefinition {
   groupName: string
   type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
   order?: 'asc' | 'desc'
-  fallbackSort?: { type: string; order?: 'asc' | 'desc' }
+  fallbackSort?: { type: string; order?: 'asc' | 'desc'; sortBy?: 'name' | 'value' }
   sortBy?: 'name' | 'value'
   newlinesInside?: 'always' | 'never'
   selector?: string
@@ -579,7 +580,7 @@ interface CustomGroupAnyOfDefinition {
   groupName: string
   type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
   order?: 'asc' | 'desc'
-  fallbackSort?: { type: string; order?: 'asc' | 'desc' }
+  fallbackSort?: { type: string; order?: 'asc' | 'desc'; sortBy?: 'name' | 'value' }
   sortBy?: 'name' | 'value'
   newlinesInside?: 'always' | 'never'
   anyOf: Array<{
diff --git a/docs/content/rules/sort-object-types.mdx b/docs/content/rules/sort-object-types.mdx
index 2bf2926..ee527cd 100644
--- a/docs/content/rules/sort-object-types.mdx
+++ b/docs/content/rules/sort-object-types.mdx
@@ -124,6 +124,7 @@ Determines whether the sorted items should be in ascending or descending order.
   {
     type: 'alphabetical' | 'natural' | 'line-length' | 'custom' | 'unsorted'
     order?: 'asc' | 'desc'
+    sortBy?: 'name' | 'value'
   }
   ```
 </sub>
@@ -507,7 +508,7 @@ interface CustomGroupDefinition {
   groupName: string
   type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
   order?: 'asc' | 'desc'
-  fallbackSort?: { type: string; order?: 'asc' | 'desc' }
+  fallbackSort?: { type: string; order?: 'asc' | 'desc'; sortBy?: 'name' | 'value' }
   sortBy?: 'name' | 'value'
   newlinesInside?: 'always' | 'never'
   selector?: string
@@ -526,7 +527,7 @@ interface CustomGroupAnyOfDefinition {
   groupName: string
   type?: 'alphabetical' | 'natural' | 'line-length' | 'unsorted'
   order?: 'asc' | 'desc'
-  fallbackSort?: { type: string; order?: 'asc' | 'desc' }
+  fallbackSort?: { type: string; order?: 'asc' | 'desc'; sortBy?: 'name' | 'value' }
   sortBy?: 'name' | 'value'
   newlinesInside?: 'always' | 'never'
   anyOf: Array<{
diff --git a/rules/sort-object-types.ts b/rules/sort-object-types.ts
index 0e82670..d28b243 100644
--- a/rules/sort-object-types.ts
+++ b/rules/sort-object-types.ts
@@ -17,7 +17,7 @@ import {
   partitionByNewLineJsonSchema,
   newlinesBetweenJsonSchema,
   customGroupsJsonSchema,
-  commonJsonSchemas,
+  buildCommonJsonSchemas,
   groupsJsonSchema,
   regexJsonSchema,
 } from '../utils/common-json-schemas'
@@ -68,7 +68,7 @@ type MESSAGE_ID =
   | 'unexpectedObjectTypesOrder'
 
 let defaultOptions: Required<Options[0]> = {
-  fallbackSort: { type: 'unsorted' },
+  fallbackSort: { type: 'unsorted', sortBy: 'name' },
   partitionByComment: false,
   partitionByNewLine: false,
   newlinesBetween: 'ignore',
@@ -89,11 +89,18 @@ let defaultOptions: Required<Options[0]> = {
 export let jsonSchema: JSONSchema4 = {
   items: {
     properties: {
-      ...commonJsonSchemas,
+      ...buildCommonJsonSchemas({
+        additionalFallbackSortProperties: {
+          sortBy: sortByJsonSchema,
+        },
+      }),
       customGroups: {
         oneOf: [
           customGroupsJsonSchema,
-          buildCustomGroupsArrayJsonSchema({ singleCustomGroupJsonSchema }),
+          buildCustomGroupsArrayJsonSchema({
+            additionalFallbackSortProperties: { sortBy: sortByJsonSchema },
+            singleCustomGroupJsonSchema,
+          }),
         ],
       },
       useConfigurationIf: buildUseConfigurationIfJsonSchema({
@@ -363,13 +370,17 @@ export let sortObjectTypeElements = <MessageIds extends string>({
       filteredGroupKindNodes.flatMap(groupedNodes =>
         sortNodesByGroups({
           getOptionsByGroupNumber: groupNumber => {
-            let { options: overriddenOptions, nodeValueGetter } =
-              getCustomGroupsCompareOptions(options, groupNumber)
+            let {
+              fallbackSortNodeValueGetter,
+              options: overriddenOptions,
+              nodeValueGetter,
+            } = getCustomGroupsCompareOptions(options, groupNumber)
             return {
               options: {
                 ...options,
                 ...overriddenOptions,
               },
+              fallbackSortNodeValueGetter,
               nodeValueGetter,
             }
           },
diff --git a/rules/sort-object-types/get-custom-groups-compare-options.ts b/rules/sort-object-types/get-custom-groups-compare-options.ts
index ae3bc95..4f594d9 100644
--- a/rules/sort-object-types/get-custom-groups-compare-options.ts
+++ b/rules/sort-object-types/get-custom-groups-compare-options.ts
@@ -17,6 +17,7 @@ export let getCustomGroupsCompareOptions = (
     Required<Options[0]>,
     'fallbackSort' | 'sortBy' | 'order' | 'type'
   >
+  fallbackSortNodeValueGetter?: NodeValueGetterFunction<SortObjectTypesSortingNode> | null
   nodeValueGetter?: NodeValueGetterFunction<SortObjectTypesSortingNode> | null
 } => {
   let baseCompareOptions = baseGetCustomGroupsCompareOptions(
@@ -24,7 +25,8 @@ export let getCustomGroupsCompareOptions = (
     groupNumber,
   )
 
-  let { customGroups, sortBy, groups } = options
+  let { fallbackSort, customGroups, sortBy, groups } = options
+  let fallbackSortBy = fallbackSort.sortBy
   if (Array.isArray(customGroups)) {
     let group = groups[groupNumber]
     let customGroup =
@@ -32,16 +34,26 @@ export let getCustomGroupsCompareOptions = (
         ? customGroups.find(currentGroup => group === currentGroup.groupName)
         : null
 
-    if (customGroup && 'sortBy' in customGroup && customGroup.sortBy) {
-      ;({ sortBy } = customGroup)
+    if (customGroup) {
+      fallbackSortBy = customGroup.fallbackSort?.sortBy ?? fallbackSortBy
+      if ('sortBy' in customGroup && customGroup.sortBy) {
+        ;({ sortBy } = customGroup)
+      }
     }
   }
 
   return {
     options: {
       ...baseCompareOptions,
+      fallbackSort: {
+        ...baseCompareOptions.fallbackSort,
+        sortBy: fallbackSortBy,
+      },
       sortBy,
     },
+    fallbackSortNodeValueGetter: fallbackSortBy
+      ? buildNodeValueGetter(fallbackSortBy)
+      : null,
     nodeValueGetter: buildNodeValueGetter(sortBy),
   }
 }
diff --git a/rules/sort-object-types/types.ts b/rules/sort-object-types/types.ts
index 8e49408..c233266 100644
--- a/rules/sort-object-types/types.ts
+++ b/rules/sort-object-types/types.ts
@@ -6,6 +6,7 @@ import type {
   PartitionByCommentOption,
   NewlinesBetweenOption,
   CustomGroupsOption,
+  FallbackSortOption,
   CommonOptions,
   GroupsOptions,
   RegexOption,
@@ -21,13 +22,19 @@ import {
 
 export type Options = Partial<
   {
+    customGroups:
+      | CustomGroupsOption<
+          SingleCustomGroup,
+          {
+            fallbackSort?: { sortBy?: 'value' | 'name' } & FallbackSortOption
+          }
+        >
+      | DeprecatedCustomGroupsOption
     useConfigurationIf: {
       declarationMatchesPattern?: RegexOption
       allNamesMatchPattern?: RegexOption
     }
-    customGroups:
-      | CustomGroupsOption<SingleCustomGroup>
-      | DeprecatedCustomGroupsOption
+    fallbackSort: { sortBy?: 'value' | 'name' } & FallbackSortOption
     /**
      * @deprecated for {@link `groups`}
      */
@@ -41,7 +48,7 @@ export type Options = Partial<
      */
     ignorePattern: RegexOption
     sortBy: 'value' | 'name'
-  } & CommonOptions
+  } & Omit<CommonOptions, 'fallbackSort'>
 >[]
 
 export type SingleCustomGroup = (
diff --git a/types/common-options.ts b/types/common-options.ts
index 0656e95..1d17e29 100644
--- a/types/common-options.ts
+++ b/types/common-options.ts
@@ -1,10 +1,14 @@
-export type CustomGroupsOption<SingleCustomGroup = object> = ({
+export type CustomGroupsOption<
+  SingleCustomGroup = object,
+  AdditionalOptions = Record<never, never>,
+> = ({
   newlinesInside?: 'always' | 'never'
   fallbackSort?: FallbackSortOption
   order?: OrderOption
   groupName: string
   type?: TypeOption
-} & (AnyOfCustomGroup<SingleCustomGroup> | SingleCustomGroup))[]
+} & (AnyOfCustomGroup<SingleCustomGroup> | SingleCustomGroup) &
+  AdditionalOptions)[]
 
 export interface CommonOptions {
   specialCharacters: SpecialCharactersOption
diff --git a/utils/common-json-schemas.ts b/utils/common-json-schemas.ts
index 09e3301..aafc0a9 100644
--- a/utils/common-json-schemas.ts
+++ b/utils/common-json-schemas.ts
@@ -45,24 +45,36 @@ let specialCharactersJsonSchema: JSONSchema4 = {
   type: 'string',
 }
 
-let fallbackSortJsonSchema: JSONSchema4 = {
+let buildFallbackSortJsonSchema = ({
+  additionalProperties,
+}: {
+  additionalProperties?: Record<string, JSONSchema4>
+} = {}): JSONSchema4 => ({
   properties: {
     order: orderJsonSchema,
     type: typeJsonSchema,
+    ...additionalProperties,
   },
   description: 'Fallback sort order.',
   type: 'object',
-}
+})
 
-export let commonJsonSchemas: Record<string, JSONSchema4> = {
+export let buildCommonJsonSchemas = ({
+  additionalFallbackSortProperties,
+}: {
+  additionalFallbackSortProperties?: Record<string, JSONSchema4>
+} = {}): Record<string, JSONSchema4> => ({
+  fallbackSort: buildFallbackSortJsonSchema(additionalFallbackSortProperties),
   specialCharacters: specialCharactersJsonSchema,
-  fallbackSort: fallbackSortJsonSchema,
   ignoreCase: ignoreCaseJsonSchema,
   alphabet: alphabetJsonSchema,
   locales: localesJsonSchema,
   order: orderJsonSchema,
   type: typeJsonSchema,
-}
+})
+
+export let commonJsonSchemas: Record<string, JSONSchema4> =
+  buildCommonJsonSchemas()
 
 export let newlinesBetweenJsonSchema: JSONSchema4 = {
   description: 'Specifies how new lines should be handled between groups.',
@@ -190,32 +202,42 @@ export let buildUseConfigurationIfJsonSchema = ({
   type: 'object',
 })
 
-let commonCustomGroupJsonSchemas: Record<string, JSONSchema4> = {
+let buildCommonCustomGroupJsonSchemas = ({
+  additionalFallbackSortProperties,
+}: {
+  additionalFallbackSortProperties?: Record<string, JSONSchema4>
+} = {}): Record<string, JSONSchema4> => ({
   newlinesInside: {
     description:
       'Specifies how new lines should be handled between members of the custom group.',
     enum: ['always', 'never'],
     type: 'string',
   },
+  fallbackSort: buildFallbackSortJsonSchema({
+    additionalProperties: additionalFallbackSortProperties,
+  }),
   groupName: {
     description: 'Custom group name.',
     type: 'string',
   },
-  fallbackSort: fallbackSortJsonSchema,
   order: orderJsonSchema,
   type: typeJsonSchema,
-}
+})
 
 export let buildCustomGroupsArrayJsonSchema = ({
+  additionalFallbackSortProperties,
   singleCustomGroupJsonSchema,
 }: {
+  additionalFallbackSortProperties?: Record<string, JSONSchema4>
   singleCustomGroupJsonSchema?: Record<string, JSONSchema4>
 }): JSONSchema4 => ({
   items: {
     oneOf: [
       {
         properties: {
-          ...commonCustomGroupJsonSchemas,
+          ...buildCommonCustomGroupJsonSchemas({
+            additionalFallbackSortProperties,
+          }),
           anyOf: {
             items: {
               properties: {
@@ -234,7 +256,9 @@ export let buildCustomGroupsArrayJsonSchema = ({
       },
       {
         properties: {
-          ...commonCustomGroupJsonSchemas,
+          ...buildCommonCustomGroupJsonSchemas({
+            additionalFallbackSortProperties,
+          }),
           ...singleCustomGroupJsonSchema,
         },
         description: 'Custom group.',
diff --git a/utils/compare.ts b/utils/compare.ts
index aecd91d..f2281c7 100644
--- a/utils/compare.ts
+++ b/utils/compare.ts
@@ -11,6 +11,7 @@ import { convertBooleanToSign } from './convert-boolean-to-sign'
 export type NodeValueGetterFunction<T extends SortingNode> = (node: T) => string
 
 interface CompareParameters<T extends SortingNode> {
+  fallbackSortNodeValueGetter?: NodeValueGetterFunction<T> | null
   options: { maxLineLength?: number } & CommonOptions
   nodeValueGetter?: NodeValueGetterFunction<T> | null
   a: T
@@ -23,6 +24,7 @@ type IndexByCharacters = Map<string, number>
 let alphabetCache = new Map<string, IndexByCharacters>()
 
 export let compare = <T extends SortingNode>({
+  fallbackSortNodeValueGetter,
   nodeValueGetter,
   options,
   a,
@@ -50,7 +52,7 @@ export let compare = <T extends SortingNode>({
       order: fallbackSort.order ?? order,
       type: fallbackSort.type,
     },
-    nodeValueGetter: finalNodeValueGetter,
+    nodeValueGetter: fallbackSortNodeValueGetter ?? finalNodeValueGetter,
     a,
     b,
   })
diff --git a/utils/sort-nodes-by-groups.ts b/utils/sort-nodes-by-groups.ts
index 3d5be63..0f23e48 100644
--- a/utils/sort-nodes-by-groups.ts
+++ b/utils/sort-nodes-by-groups.ts
@@ -14,6 +14,7 @@ interface SortNodesByGroupsParameters<
   T extends SortingNode,
 > {
   getOptionsByGroupNumber(groupNumber: number): {
+    fallbackSortNodeValueGetter?: NodeValueGetterFunction<T> | null
     nodeValueGetter?: NodeValueGetterFunction<T> | null
     options: Options
   }
@@ -54,9 +55,8 @@ export let sortNodesByGroups = <
   for (let groupNumber of Object.keys(nodesByNonIgnoredGroupNumber).sort(
     (a, b) => Number(a) - Number(b),
   )) {
-    let { nodeValueGetter, options } = getOptionsByGroupNumber(
-      Number(groupNumber),
-    )
+    let { fallbackSortNodeValueGetter, nodeValueGetter, options } =
+      getOptionsByGroupNumber(Number(groupNumber))
     let nodesToPush = nodesByNonIgnoredGroupNumber[Number(groupNumber)]!
 
     let groupIgnoredNodes = new Set(
@@ -67,6 +67,7 @@ export let sortNodesByGroups = <
       ...sortNodes({
         isNodeIgnored: node => groupIgnoredNodes.has(node),
         ignoreEslintDisabledNodes: false,
+        fallbackSortNodeValueGetter,
         nodes: nodesToPush,
         nodeValueGetter,
         options,
diff --git a/utils/sort-nodes.ts b/utils/sort-nodes.ts
index 5a4a323..abae9ad 100644
--- a/utils/sort-nodes.ts
+++ b/utils/sort-nodes.ts
@@ -5,6 +5,7 @@ import type { SortingNode } from '../types/sorting-node'
 import { compare } from './compare'
 
 interface SortNodesParameters<T extends SortingNode> {
+  fallbackSortNodeValueGetter?: NodeValueGetterFunction<T> | null
   options: { maxLineLength?: number } & CommonOptions
   nodeValueGetter?: NodeValueGetterFunction<T> | null
   ignoreEslintDisabledNodes: boolean
@@ -13,6 +14,7 @@ interface SortNodesParameters<T extends SortingNode> {
 }
 
 export let sortNodes = <T extends SortingNode>({
+  fallbackSortNodeValueGetter,
   ignoreEslintDisabledNodes,
   nodeValueGetter,
   isNodeIgnored,
@@ -34,6 +36,7 @@ export let sortNodes = <T extends SortingNode>({
 
   let sortedNodes = [...nonIgnoredNodes].sort((a, b) =>
     compare({
+      fallbackSortNodeValueGetter,
       nodeValueGetter,
       options,
       a,


Test patch:
diff --git a/test/rules/sort-interfaces.test.ts b/test/rules/sort-interfaces.test.ts
index f34868d..4ec0c38 100644
--- a/test/rules/sort-interfaces.test.ts
+++ b/test/rules/sort-interfaces.test.ts
@@ -1303,6 +1303,47 @@ describe(ruleName, () => {
                 }
               `,
             },
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      fallbackSort: {
+                        type: 'alphabetical',
+                        sortBy: 'value',
+                      },
+                      elementValuePattern: '^foo',
+                      type: 'line-length',
+                      groupName: 'foo',
+                    },
+                  ],
+                  type: 'alphabetical',
+                  groups: ['foo'],
+                  order: 'asc',
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    right: 'b',
+                    left: 'a',
+                  },
+                  messageId: 'unexpectedInterfacePropertiesOrder',
+                },
+              ],
+              output: dedent`
+                interface Interface {
+                  b: fooBar
+                  a: fooZar
+                }
+              `,
+              code: dedent`
+                interface Interface {
+                  a: fooZar
+                  b: fooBar
+                }
+              `,
+            },
           ],
           valid: [],
         },
@@ -5048,6 +5089,40 @@ describe(ruleName, () => {
               }
             `,
           },
+          {
+            errors: [
+              {
+                data: {
+                  right: 'bb',
+                  left: 'c',
+                },
+                messageId: 'unexpectedInterfacePropertiesOrder',
+              },
+            ],
+            options: [
+              {
+                ...options,
+                fallbackSort: {
+                  type: 'alphabetical',
+                  sortBy: 'value',
+                },
+              },
+            ],
+            output: dedent`
+              interface Interface {
+                bb: string;
+                c: boolean;
+                a: number;
+              }
+            `,
+            code: dedent`
+              interface Interface {
+                c: boolean;
+                bb: string;
+                a: number;
+              }
+            `,
+          },
         ],
         valid: [],
       },
diff --git a/test/rules/sort-object-types.test.ts b/test/rules/sort-object-types.test.ts
index bb7dc5e..3b15323 100644
--- a/test/rules/sort-object-types.test.ts
+++ b/test/rules/sort-object-types.test.ts
@@ -1044,6 +1044,47 @@ describe(ruleName, () => {
                 }
               `,
             },
+            {
+              options: [
+                {
+                  customGroups: [
+                    {
+                      fallbackSort: {
+                        type: 'alphabetical',
+                        sortBy: 'value',
+                      },
+                      elementValuePattern: '^foo',
+                      type: 'line-length',
+                      groupName: 'foo',
+                    },
+                  ],
+                  type: 'alphabetical',
+                  groups: ['foo'],
+                  order: 'asc',
+                },
+              ],
+              errors: [
+                {
+                  data: {
+                    right: 'b',
+                    left: 'a',
+                  },
+                  messageId: 'unexpectedObjectTypesOrder',
+                },
+              ],
+              output: dedent`
+                type Type = {
+                  b: fooBar
+                  a: fooZar
+                }
+              `,
+              code: dedent`
+                type Type = {
+                  a: fooZar
+                  b: fooBar
+                }
+              `,
+            },
           ],
           valid: [],
         },
@@ -4328,6 +4369,40 @@ describe(ruleName, () => {
               }
             `,
           },
+          {
+            errors: [
+              {
+                data: {
+                  right: 'bb',
+                  left: 'c',
+                },
+                messageId: 'unexpectedObjectTypesOrder',
+              },
+            ],
+            options: [
+              {
+                ...options,
+                fallbackSort: {
+                  type: 'alphabetical',
+                  sortBy: 'value',
+                },
+              },
+            ],
+            output: dedent`
+              type Type = {
+                bb: string;
+                c: boolean;
+                a: number;
+              }
+            `,
+            code: dedent`
+              type Type = {
+                c: boolean;
+                bb: string;
+                a: number;
+              }
+            `,
+          },
         ],
         valid: [],
       },
diff --git a/test/rules/sort-object-types/get-custom-groups-compare-options.test.ts b/test/rules/sort-object-types/get-custom-groups-compare-options.test.ts
new file mode 100644
index 0000000..f459dcc
--- /dev/null
+++ b/test/rules/sort-object-types/get-custom-groups-compare-options.test.ts
@@ -0,0 +1,199 @@
+import { describe, expect, it } from 'vitest'
+
+import type { Options } from '../../../rules/sort-object-types/types'
+
+import { getCustomGroupsCompareOptions } from '../../../rules/sort-object-types/get-custom-groups-compare-options'
+
+describe('get-custom-groups-compare-options', () => {
+  let commonOptions: Pick<
+    Required<Options[0]>,
+    'fallbackSort' | 'sortBy' | 'order' | 'type'
+  > = {
+    fallbackSort: {
+      type: 'unsorted',
+      sortBy: 'name',
+    },
+    type: 'alphabetical',
+    sortBy: 'name',
+    order: 'asc',
+  }
+
+  it('return the entered options if "customGroups" is not an array', () => {
+    expect(
+      getCustomGroupsCompareOptions(
+        {
+          ...commonOptions,
+          groups: ['group'],
+          customGroups: {},
+        },
+        0,
+      ),
+    ).toStrictEqual({
+      fallbackSortNodeValueGetter: null,
+      options: commonOptions,
+      nodeValueGetter: null,
+    })
+  })
+
+  it('return the entered options if the group is not linked to a custom group', () => {
+    expect(
+      getCustomGroupsCompareOptions(
+        {
+          ...commonOptions,
+          groups: ['group'],
+          customGroups: [],
+        },
+        0,
+      ),
+    ).toStrictEqual({
+      fallbackSortNodeValueGetter: null,
+      options: commonOptions,
+      nodeValueGetter: null,
+    })
+  })
+
+  describe('"fallbackSort"', () => {
+    it('overrides "fallbackSort.type"', () => {
+      expect(
+        getCustomGroupsCompareOptions(
+          {
+            ...commonOptions,
+            customGroups: [
+              {
+                fallbackSort: {
+                  type: 'unsorted',
+                },
+                groupName: 'group',
+              },
+            ],
+            groups: ['group'],
+          },
+          0,
+        ),
+      ).toStrictEqual({
+        options: {
+          ...commonOptions,
+          fallbackSort: {
+            ...commonOptions.fallbackSort,
+            type: 'unsorted',
+          },
+        },
+        fallbackSortNodeValueGetter: null,
+        nodeValueGetter: null,
+      })
+    })
+
+    it('overrides "fallbackSort.order"', () => {
+      expect(
+        getCustomGroupsCompareOptions(
+          {
+            ...commonOptions,
+            customGroups: [
+              {
+                fallbackSort: {
+                  type: 'alphabetical',
+                  order: 'desc',
+                },
+                groupName: 'group',
+              },
+            ],
+            groups: ['group'],
+          },
+          0,
+        ),
+      ).toStrictEqual({
+        options: {
+          ...commonOptions,
+          fallbackSort: {
+            ...commonOptions.fallbackSort,
+            type: 'alphabetical',
+            order: 'desc',
+          },
+        },
+        fallbackSortNodeValueGetter: null,
+        nodeValueGetter: null,
+      })
+    })
+
+    it('overrides "fallbackSort.sortBy"', () => {
+      expect(
+        getCustomGroupsCompareOptions(
+          {
+            ...commonOptions,
+            customGroups: [
+              {
+                fallbackSort: {
+                  type: 'alphabetical',
+                  sortBy: 'value',
+                },
+                groupName: 'group',
+              },
+            ],
+            groups: ['group'],
+          },
+          0,
+        ),
+      ).toStrictEqual({
+        options: {
+          ...commonOptions,
+          fallbackSort: {
+            type: 'alphabetical',
+            sortBy: 'value',
+          },
+        },
+        fallbackSortNodeValueGetter: expect.any(Function),
+        nodeValueGetter: null,
+      })
+    })
+  })
+
+  it('overrides "type"', () => {
+    expect(
+      getCustomGroupsCompareOptions(
+        {
+          ...commonOptions,
+          customGroups: [
+            {
+              groupName: 'group',
+              type: 'unsorted',
+            },
+          ],
+          groups: ['group'],
+        },
+        0,
+      ),
+    ).toStrictEqual({
+      options: {
+        ...commonOptions,
+        type: 'unsorted',
+      },
+      fallbackSortNodeValueGetter: null,
+      nodeValueGetter: null,
+    })
+  })
+
+  it('overrides "order"', () => {
+    expect(
+      getCustomGroupsCompareOptions(
+        {
+          ...commonOptions,
+          customGroups: [
+            {
+              groupName: 'group',
+              order: 'desc',
+            },
+          ],
+          groups: ['group'],
+        },
+        0,
+      ),
+    ).toStrictEqual({
+      options: {
+        ...commonOptions,
+        order: 'desc',
+      },
+      fallbackSortNodeValueGetter: null,
+      nodeValueGetter: null,
+    })
+  })
+})
diff --git a/test/utils/compare.test.ts b/test/utils/compare.test.ts
index 720ea0c..302a677 100644
--- a/test/utils/compare.test.ts
+++ b/test/utils/compare.test.ts
@@ -426,6 +426,32 @@ describe('compare', () => {
       ).toBe(-1)
     })
 
+    it('handles `fallbackSort.nodeValueGetter`', () => {
+      let a = createTestNode({
+        additionalProperties: { value: 'b' },
+        name: 'aaa',
+      })
+      let b = createTestNode({
+        additionalProperties: { value: 'a' },
+        name: 'bbb',
+      })
+      expect(
+        compare({
+          options: {
+            ...options,
+            fallbackSort: {
+              type: 'alphabetical',
+              order: 'asc',
+            },
+          },
+          fallbackSortNodeValueGetter: node =>
+            'value' in node ? (node.value as string) : '',
+          a,
+          b,
+        }),
+      ).toBe(1)
+    })
+
     it("doesn't sort using the fallback configuration more than once", () => {
       let node = createTestNode({ name: 'aaa' })
       let duplicateNode = createTestNode({ name: 'aaa' })
diff --git a/test/utils/get-custom-groups-compare-options.test.ts b/test/utils/get-custom-groups-compare-options.test.ts
index 82805ce..174c3b0 100644
--- a/test/utils/get-custom-groups-compare-options.test.ts
+++ b/test/utils/get-custom-groups-compare-options.test.ts
@@ -43,28 +43,57 @@ describe('get-custom-groups-compare-options', () => {
     })
   })
 
-  it('overrides "fallbackSort"', () => {
-    expect(
-      getCustomGroupsCompareOptions(
-        {
-          ...baseOptions,
-          customGroups: [
-            {
-              fallbackSort: {
-                type: 'unsorted',
+  describe('"fallbackSort"', () => {
+    it('overrides "fallbackSort.type"', () => {
+      expect(
+        getCustomGroupsCompareOptions(
+          {
+            ...baseOptions,
+            customGroups: [
+              {
+                fallbackSort: {
+                  type: 'unsorted',
+                },
+                groupName: 'group',
               },
-              groupName: 'group',
-            },
-          ],
-          groups: ['group'],
+            ],
+            groups: ['group'],
+          },
+          0,
+        ),
+      ).toStrictEqual({
+        ...baseOptions,
+        fallbackSort: {
+          type: 'unsorted',
         },
-        0,
-      ),
-    ).toStrictEqual({
-      ...baseOptions,
-      fallbackSort: {
-        type: 'unsorted',
-      },
+      })
+    })
+
+    it('overrides "fallbackSort.order"', () => {
+      expect(
+        getCustomGroupsCompareOptions(
+          {
+            ...baseOptions,
+            customGroups: [
+              {
+                fallbackSort: {
+                  type: 'alphabetical',
+                  order: 'desc',
+                },
+                groupName: 'group',
+              },
+            ],
+            groups: ['group'],
+          },
+          0,
+        ),
+      ).toStrictEqual({
+        ...baseOptions,
+        fallbackSort: {
+          type: 'alphabetical',
+          order: 'desc',
+        },
+      })
     })
   })
 
@@ -85,7 +114,6 @@ describe('get-custom-groups-compare-options', () => {
       ),
     ).toStrictEqual({
       ...baseOptions,
-
       fallbackSort: {
         type: 'unsorted',
       },
@@ -110,7 +138,6 @@ describe('get-custom-groups-compare-options', () => {
       ),
     ).toStrictEqual({
       ...baseOptions,
-
       fallbackSort: {
         type: 'unsorted',
       },
diff --git a/test/utils/sort-nodes-by-groups.test.ts b/test/utils/sort-nodes-by-groups.test.ts
index 719c3d3..69ee4fe 100644
--- a/test/utils/sort-nodes-by-groups.test.ts
+++ b/test/utils/sort-nodes-by-groups.test.ts
@@ -127,6 +127,33 @@ describe('sort-nodes-by-groups', () => {
     ).toStrictEqual([nodeB, nodeA])
   })
 
+  it('should handle "fallbackSortNodeValueGetter"', () => {
+    let nodeA = createTestNode(
+      { group: 'group1', name: 'a' },
+      { actualValue: 'b' },
+    )
+    let nodeB = createTestNode(
+      { group: 'group1', name: 'a' },
+      { actualValue: 'a' },
+    )
+    expect(
+      sortNodesByGroups({
+        getOptionsByGroupNumber: () => ({
+          options: {
+            ...options,
+            fallbackSort: {
+              type: 'alphabetical',
+            },
+          },
+          fallbackSortNodeValueGetter: node => node.actualValue,
+        }),
+        ignoreEslintDisabledNodes: false,
+        nodes: [nodeA, nodeB],
+        groups: ['group1'],
+      }),
+    ).toStrictEqual([nodeB, nodeA])
+  })
+
   let createTestNode = <T extends object>(
     node: {
       isEslintDisabled?: boolean
diff --git a/test/utils/sort-nodes.test.ts b/test/utils/sort-nodes.test.ts
index ada8915..48a677d 100644
--- a/test/utils/sort-nodes.test.ts
+++ b/test/utils/sort-nodes.test.ts
@@ -82,6 +82,24 @@ describe('sort-nodes', () => {
     ).toStrictEqual([nodeB, nodeA])
   })
 
+  it('should handle "fallbackSortNodeValueGetter"', () => {
+    let nodeA = createTestNode({ name: 'a' }, { actualValue: 'b' })
+    let nodeB = createTestNode({ name: 'a' }, { actualValue: 'a' })
+    expect(
+      sortNodes({
+        options: {
+          ...options,
+          fallbackSort: {
+            type: 'alphabetical',
+          },
+        },
+        fallbackSortNodeValueGetter: node => node.actualValue,
+        ignoreEslintDisabledNodes: false,
+        nodes: [nodeA, nodeB],
+      }),
+    ).toStrictEqual([nodeB, nodeA])
+  })
+
   let createTestNode = <T extends object>(
     node: {
       isEslintDisabled?: boolean