Repository: azat-io/eslint-plugin-perfectionist (ts)

Issue:
Feature: `sort-objects` Add an option to disable destructured objects sorting
### What rule do you want to change?

sort-objects

### Describe the problem

When using `sort-objects` rule with `customGroups`, the rule is also applied to destructured objects. I need custom groups to be used only for objects and no sorting (or also customizable one) for destructured objects.

### Code example

Given the config:

```js
import perfectionist from 'eslint-plugin-perfectionist'

export default {
  plugins: {
    perfectionist
  },
  rules: {
    'perfectionist/sort-objects': ['error', {
      customGroups: {
        id: '^id$'
      },
      groups: [
        'id',
        'unknown'
      ],
      type: 'natural'
    }]
  }
}
```

✅ Object is sorted perfectly fine with `id` being first:

```js
const movie = {
  id: 1,
  createdAt: 2024,
  title: 'movie'
}
```

❌ However, function with destructured object also follows the same rules and will give an error:

```js
// Expected "id" (id) to come before "createdAt" (unknown).
function execute({ createdAt, id, title }) {}
``` 

Same goes for React functional components where using destructured `props` object is a common pattern:

```jsx
// Expected "id" (id) to come before "children" (unknown).
export default function Task({ children, id, title }) {
  return (
    <p>hello darkness my old friend</p>
  )
}
```

### Additional comments

There is an option `destructureOnly` which applies the rule only to destructured objects and I kinda need the opposite.

I've also used [eslint-plugin-sort-destructure-keys](https://github.com/mthadley/eslint-plugin-sort-destructure-keys) for sorting destructured objects so it would be a perfect combo.

### Validations

- [X] Read the [docs](https://perfectionist.dev).
- [X] Check that there isn't [already an issue](https://github.com/azat-io/eslint-plugin-perfectionist/issues) that reports the same bug to avoid creating a duplicate.

Pull request:
feat(sort-objects): adds `objectDeclarations ` and `destructuredObjects ` options
Resolves #392

## Changes

- Deprecates the `destructureOnly` option in the documentation (still usable).
- Adds a `objectDeclarations: boolean` option (by default `true`): when false, will not sort object declarations. 
- Adds a `destructuredObjects: boolean | { groups: boolean }` option (by default `true`): when false, will not sort destructured objects. When `{ groups: false }`, will not use groups to sort destructured objects.

## Advantages of using `{ groups: boolean }` rather than `{ disableGrouping: boolean }`

- Uses the same existing option name `groups`, making it clear that this sub-option is linked with the parent `groups` option.
- Gives flexibility later if users require specifying specific grouping for destructured objects: `{ groups: boolean }` can be extended to `{ groups: boolean | string[] | string[][] }`.

### What is the purpose of this pull request?

- [x] New Feature


Patch:
diff --git a/docs/content/rules/sort-objects.mdx b/docs/content/rules/sort-objects.mdx
index ec2dd2f..29369fc 100644
--- a/docs/content/rules/sort-objects.mdx
+++ b/docs/content/rules/sort-objects.mdx
@@ -264,11 +264,29 @@ Allows you to specify names or patterns for object types that should be ignored
 
 You can specify their names or a regexp pattern to ignore, for example: `'^User.+'` to ignore all object types whose names begin with the word “User”.
 
-### destructureOnly
+### [DEPRECATED] destructureOnly
 
 <sub>default: `false`</sub>
 
-Allows you to sort only objects that are part of a destructuring pattern. When set to `true`, the rule will apply sorting exclusively to destructured objects, leaving other object declarations unchanged.
+Use the [objectDeclarations](#objectdeclarations) and [destructuredObjects](#destructuredobjects) options instead.
+
+Allows you to only sort objects that are part of a destructuring pattern. When set to `true`, the rule will apply sorting exclusively to destructured objects, leaving other object declarations unchanged.
+
+### objectDeclarations
+
+<sub>default: `true`</sub>
+
+Allows you to choose whether to sort standard object declarations.
+
+### destructuredObjects
+
+<sub>
+  type: `boolean | { groups: boolean }`
+</sub>
+<sub>default: `true`</sub>
+
+Allows you to choose whether to sort destructured objects.
+The `groups` attribute allows you to specify whether to use groups to sort destructured objects.
 
 ### groups
 
diff --git a/docs/env.d.ts b/docs/env.d.ts
index b5d2c88..0fca85a 100644
--- a/docs/env.d.ts
+++ b/docs/env.d.ts
@@ -7,11 +7,3 @@ declare module '*.svg?raw' {
   let content: string
   export default content
 }
-
-declare module 'remark-sectionize' {
-  import type { Plugin } from 'unified'
-  import type { Root } from 'mdast'
-
-  let plugin: Plugin<object, Root>
-  export default plugin
-}
diff --git a/rules/sort-objects.ts b/rules/sort-objects.ts
index 9544822..130a0ed 100644
--- a/rules/sort-objects.ts
+++ b/rules/sort-objects.ts
@@ -37,12 +37,14 @@ 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 { sortNodes } from '../utils/sort-nodes'
 import { complete } from '../utils/complete'
 import { pairwise } from '../utils/pairwise'
 import { matches } from '../utils/matches'
 
 type Options = [
   Partial<{
+    destructuredObjects: { groups: boolean } | boolean
     type: 'alphabetical' | 'line-length' | 'natural'
     customGroups: Record<string, string[] | string>
     partitionByComment: string[] | boolean | string
@@ -51,7 +53,11 @@ type Options = [
     locales: NonNullable<Intl.LocalesArgument>
     groups: (Group[] | Group)[]
     partitionByNewLine: boolean
+    objectDeclarations: boolean
     styledComponents: boolean
+    /**
+     * @deprecated for {@link `destructuredObjects`} and {@link `objectDeclarations`}
+     */
     destructureOnly: boolean
     ignorePattern: string[]
     order: 'desc' | 'asc'
@@ -73,6 +79,8 @@ let defaultOptions: Required<Options[0]> = {
   partitionByComment: false,
   newlinesBetween: 'ignore',
   specialCharacters: 'keep',
+  destructuredObjects: true,
+  objectDeclarations: true,
   styledComponents: true,
   destructureOnly: false,
   type: 'alphabetical',
@@ -87,8 +95,12 @@ let defaultOptions: Required<Options[0]> = {
 export default createEslintRule<Options, MESSAGE_ID>({
   create: context => {
     let sortObject = (
-      node: TSESTree.ObjectExpression | TSESTree.ObjectPattern,
+      nodeObject: TSESTree.ObjectExpression | TSESTree.ObjectPattern,
     ): void => {
+      if (!isSortable(nodeObject.properties)) {
+        return
+      }
+
       let settings = getSettings(context.settings)
 
       let options = complete(context.options.at(0), settings, defaultOptions)
@@ -100,14 +112,17 @@ export default createEslintRule<Options, MESSAGE_ID>({
       )
       validateNewlinesAndPartitionConfiguration(options)
 
-      let shouldIgnore = false
-
-      if (options.destructureOnly) {
-        shouldIgnore = node.type !== 'ObjectPattern'
+      let isDestructuredObject = nodeObject.type === 'ObjectPattern'
+      if (isDestructuredObject) {
+        if (!options.destructuredObjects) {
+          return
+        }
+      } else if (options.destructureOnly || !options.objectDeclarations) {
+        return
       }
 
-      if (!shouldIgnore && options.ignorePattern.length) {
-        let variableParent = getNodeParent(node, [
+      if (options.ignorePattern.length) {
+        let variableParent = getNodeParent(nodeObject, [
           'VariableDeclarator',
           'Property',
         ])
@@ -126,10 +141,10 @@ export default createEslintRule<Options, MESSAGE_ID>({
           typeof variableIdentifier === 'string' &&
           checkMatch(variableIdentifier)
         ) {
-          shouldIgnore = true
+          return
         }
 
-        let callParent = getNodeParent(node, ['CallExpression'])
+        let callParent = getNodeParent(nodeObject, ['CallExpression'])
         let callIdentifier =
           callParent?.type === 'CallExpression' &&
           callParent.callee.type === 'Identifier'
@@ -137,12 +152,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
             : null
 
         if (callIdentifier && checkMatch(callIdentifier)) {
-          shouldIgnore = true
+          return
         }
       }
-      if (shouldIgnore || !isSortable(node.properties)) {
-        return
-      }
 
       let isStyledCallExpression = (identifier: TSESTree.Expression): boolean =>
         identifier.type === 'Identifier' && identifier.name === 'styled'
@@ -163,9 +175,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
             styledNode.parent.name.name === 'style'))
       if (
         !options.styledComponents &&
-        (isStyledComponents(node.parent) ||
-          (node.parent.type === 'ArrowFunctionExpression' &&
-            isStyledComponents(node.parent.parent)))
+        (isStyledComponents(nodeObject.parent) ||
+          (nodeObject.parent.type === 'ArrowFunctionExpression' &&
+            isStyledComponents(nodeObject.parent.parent)))
       ) {
         return
       }
@@ -352,14 +364,20 @@ export default createEslintRule<Options, MESSAGE_ID>({
           },
           [[]],
         )
-      let formattedMembers = formatProperties(node.properties)
-
+      let formattedMembers = formatProperties(nodeObject.properties)
+
+      let nodesSortingFunction =
+        isDestructuredObject &&
+        typeof options.destructuredObjects === 'object' &&
+        !options.destructuredObjects.groups
+          ? sortNodes
+          : sortNodesByGroups
       let sortNodesIgnoringEslintDisabledNodes = (
         ignoreEslintDisabledNodes: boolean,
       ): SortingNodeWithDependencies[] =>
         sortNodesByDependencies(
           formattedMembers.flatMap(nodes =>
-            sortNodesByGroups(nodes, options, {
+            nodesSortingFunction(nodes, options, {
               ignoreEslintDisabledNodes,
             }),
           ),
@@ -460,6 +478,25 @@ export default createEslintRule<Options, MESSAGE_ID>({
     schema: [
       {
         properties: {
+          destructuredObjects: {
+            oneOf: [
+              {
+                type: 'boolean',
+              },
+              {
+                properties: {
+                  groups: {
+                    description:
+                      'Controls whether to use groups to sort destructured objects.',
+                    type: 'boolean',
+                  },
+                },
+                additionalProperties: false,
+                type: 'object',
+              },
+            ],
+            description: 'Controls whether to sort destructured objects.',
+          },
           ignorePattern: {
             description:
               'Specifies names or patterns for nodes that should be ignored by rule.',
@@ -477,6 +514,10 @@ export default createEslintRule<Options, MESSAGE_ID>({
             description: 'Controls whether to sort only destructured objects.',
             type: 'boolean',
           },
+          objectDeclarations: {
+            description: 'Controls whether to sort object declarations.',
+            type: 'boolean',
+          },
           styledComponents: {
             description: 'Controls whether to sort styled components.',
             type: 'boolean',


Test patch:
diff --git a/test/sort-objects.test.ts b/test/sort-objects.test.ts
index e8cf82b..d1d4826 100644
--- a/test/sort-objects.test.ts
+++ b/test/sort-objects.test.ts
@@ -3850,7 +3850,7 @@ describe(ruleName, () => {
       ],
     })
 
-    ruleTester.run(`${ruleName}: allow to use for destructuring only`, rule, {
+    ruleTester.run(`${ruleName}: allow to use 'destructureOnly'`, rule, {
       invalid: [
         {
           errors: [
@@ -3914,6 +3914,202 @@ describe(ruleName, () => {
       ],
     })
 
+    ruleTester.run(`${ruleName}: allow to use 'objectDeclarations'`, rule, {
+      invalid: [
+        {
+          errors: [
+            {
+              data: {
+                right: 'b',
+                left: 'c',
+              },
+              messageId: 'unexpectedObjectsOrder',
+            },
+            {
+              data: {
+                right: 'a',
+                left: 'b',
+              },
+              messageId: 'unexpectedObjectsOrder',
+            },
+          ],
+          output: dedent`
+            let obj = {
+              c: 'c',
+              a: 'a',
+              b: 'b',
+            }
+
+            let { a, b, c } = obj
+          `,
+          code: dedent`
+            let obj = {
+              c: 'c',
+              a: 'a',
+              b: 'b',
+            }
+
+            let { c, b, a } = obj
+          `,
+          options: [
+            {
+              objectDeclarations: false,
+            },
+          ],
+        },
+      ],
+      valid: [
+        {
+          code: dedent`
+            let obj = {
+              c: 'c',
+              a: 'a',
+              b: 'b',
+            }
+
+            let { a, b, c } = obj
+          `,
+          options: [
+            {
+              objectDeclarations: false,
+            },
+          ],
+        },
+      ],
+    })
+
+    describe(`${ruleName}: allow to use 'destructuredObjects'`, () => {
+      ruleTester.run(`${ruleName}: boolean 'destructuredObjects'`, rule, {
+        invalid: [
+          {
+            errors: [
+              {
+                data: {
+                  right: 'b',
+                  left: 'c',
+                },
+                messageId: 'unexpectedObjectsOrder',
+              },
+              {
+                data: {
+                  right: 'a',
+                  left: 'b',
+                },
+                messageId: 'unexpectedObjectsOrder',
+              },
+            ],
+            output: dedent`
+              let obj = {
+                a: 'a',
+                b: 'b',
+                c: 'c',
+              }
+
+              let { c, a, b } = obj
+            `,
+            code: dedent`
+              let obj = {
+                c: 'c',
+                b: 'b',
+                a: 'a',
+              }
+
+              let { c, a, b } = obj
+            `,
+            options: [
+              {
+                destructuredObjects: false,
+              },
+            ],
+          },
+        ],
+        valid: [
+          {
+            code: dedent`
+              let obj = {
+                a: 'a',
+                b: 'b',
+                c: 'c',
+              }
+
+              let { b, c, a } = obj
+            `,
+            options: [
+              {
+                destructuredObjects: false,
+              },
+            ],
+          },
+        ],
+      })
+
+      ruleTester.run(
+        `${ruleName}: object 'destructuredObjects': 'groups' attribute`,
+        rule,
+        {
+          invalid: [
+            {
+              errors: [
+                {
+                  data: {
+                    leftGroup: 'unknown',
+                    rightGroup: 'top',
+                    right: 'c',
+                    left: 'a',
+                  },
+                  messageId: 'unexpectedObjectsGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  customGroups: {
+                    top: 'c',
+                  },
+                  destructuredObjects: { groups: true },
+                  groups: ['top', 'unknown'],
+                },
+              ],
+              output: dedent`
+                let { c, a, b } = obj
+              `,
+              code: dedent`
+                let { a, c, b } = obj
+              `,
+            },
+            {
+              errors: [
+                {
+                  data: {
+                    rightGroup: 'unknown',
+                    leftGroup: 'top',
+                    right: 'b',
+                    left: 'c',
+                  },
+                  messageId: 'unexpectedObjectsGroupOrder',
+                },
+              ],
+              options: [
+                {
+                  customGroups: {
+                    top: 'c',
+                  },
+                  destructuredObjects: { groups: false },
+                  groups: ['top', 'unknown'],
+                },
+              ],
+              output: dedent`
+                let { a, b, c } = obj
+              `,
+              code: dedent`
+                let { a, c, b } = obj
+              `,
+            },
+          ],
+          valid: [],
+        },
+      )
+    })
+
     ruleTester.run(`${ruleName}: works with settings`, rule, {
       valid: [
         {