Repository: azat-io/eslint-plugin-perfectionist
Pull request #511 (2025-04-12 13:03:14)

feat(sort-imports): update default groups to new API
- Follow-up of https://github.com/azat-io/eslint-plugin-perfectionist/pull/505.
- Related to the following issues:
  - https://github.com/azat-io/eslint-plugin-perfectionist/issues/494.
  - https://github.com/azat-io/eslint-plugin-perfectionist/issues/510.

# Description

This PR adds a `value` modifier alongside an `import` selector in order for the `groups` option not to rely on deprecated selectors.

## `import` selector

This selector has the lowest priority of all selectors and will match every import.

## `value` modifier

Similar to the `type` modifier. Any non-`type` import will have the `value` modifier, even `side-effect` imports.

## Tests impacted

No input/output impact. Only error messages had to be updated.

### What is the purpose of this pull request?

- [x] New Feature


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

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


```diff
@@            Coverage Diff             @@
##              main      #511    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           96        97     +1     
  Lines         7743      7848   +105     
  Branches      1556      1578    +22     
==========================================
+ Hits          7743      7848   +105     
```

</details>

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

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

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

Patch:
diff --git a/docs/content/rules/sort-imports.mdx b/docs/content/rules/sort-imports.mdx
index 70c892b..1e98c87 100644
--- a/docs/content/rules/sort-imports.mdx
+++ b/docs/content/rules/sort-imports.mdx
@@ -282,12 +282,12 @@ Specifies the  directory of the root `tsconfig.json` file (ex: `.`). This is use
   default:
   ```
   [
-    'type',
-    ['builtin', 'external'],
-    'internal-type',
-    'internal',
-    ['parent-type', 'sibling-type', 'index-type'],
-    ['parent', 'sibling', 'index'],
+    'type-import',
+    ['value-builtin', 'value-external'],
+    'type-internal',
+    'value-internal',
+    ['type-parent', 'type-sibling', 'type-index'],
+    ['value-parent', 'value-sibling', 'value-index'],
     'unknown',
   ]
   ```
@@ -319,10 +319,12 @@ The list of selectors is sorted from most to least important:
 - `'internal'` — Your internal modules.
 - `'builtin'` — Node.js Built-in Modules.
 - `'external'` — External modules installed in the project.
+- `'import'` — Any import.
 
 #### Modifiers
 
 - `'type'` — Typescript time imports.
+- `'value'` — Value imports.
 
 #### Important notes
 
@@ -349,27 +351,29 @@ import type { FC } from 'react'
 - `type` (`type` selector).
 - `type-external` (`type` modifier).
 - `external`.
+- `type-import`.
+- `import`.
 
 Example 2 (The most important group is written in the comments):
 
 ```ts
-// 'builtin' - Node.js Built-in Modules
+// 'value-builtin' - Node.js Built-in Modules
 import path from 'path'
-// 'external' - External modules installed in the project
+// 'value-external' - External modules installed in the project
 import axios from 'axios'
-// 'internal' - Your internal modules
+// 'value-internal' - Your internal modules
 import Button from '~/components/Button'
-// 'parent' - Modules from parent directory
+// 'value-parent' - Modules from parent directory
 import formatNumber from '../utils/format-number'
-// 'sibling' - Modules from the same directory
+// 'value-sibling' - Modules from the same directory
 import config from './config'
-// 'side-effect' - Side effect imports
+// 'value-side-effect' - Side effect imports
 import './set-production-env.js'
-// side-effect-style - Side effect style imports
+// value-side-effect-style - Side effect style imports
 import './styles.scss'
-// 'index' - Main file from the current directory
+// 'value-index' - Main file from the current directory
 import main from '.'
-// 'style' - Styles
+// 'value-style' - Styles
 import styles from './index.module.css'
 // 'type-external' - TypeScript type imports
 import type { FC } from 'react'
@@ -516,13 +520,13 @@ you must write a custom group definition that does the same as what the predefin
 ```js
    groups: [
 +    'react',                           // [!code ++]
-     'type',
-     ['builtin', 'external'],
+     'type-import',
+     ['value-builtin', 'value-external'],
 +    'lodash',                          // [!code ++]
-     'internal-type',
-     'internal',
-     ['parent-type', 'sibling-type', 'index-type'],
-     ['parent', 'sibling', 'index'],
+     'type-internal',
+     'value-internal',
+     ['type-parent', 'type-sibling', 'type-index'],
+     ['value-parent', 'value-sibling', 'value-index'],
      'unknown',
    ],
 +  customGroups: [                      // [!code ++]
@@ -573,12 +577,12 @@ Specifies which environment’s built-in modules should be recognized. If you ar
                   newlinesBetween: 'always',
                   maxLineLength: undefined,
                   groups: [
-                    'type',
-                    ['builtin', 'external'],
-                    'internal-type',
-                    'internal',
-                    ['parent-type', 'sibling-type', 'index-type'],
-                    ['parent', 'sibling', 'index'],
+                    'type-import',
+                    ['value-builtin', 'value-external'],
+                    'type-internal',
+                    'value-internal',
+                    ['type-parent', 'type-sibling', 'type-index'],
+                    ['value-parent', 'value-sibling', 'value-index'],
                     'unknown',
                   ],
                   customGroups: [],
@@ -614,12 +618,12 @@ Specifies which environment’s built-in modules should be recognized. If you ar
                 newlinesBetween: 'always',
                 maxLineLength: undefined,
                 groups: [
-                  'type',
-                  ['builtin', 'external'],
-                  'internal-type',
-                  'internal',
-                  ['parent-type', 'sibling-type', 'index-type'],
-                  ['parent', 'sibling', 'index'],
+                  'type-import',
+                  ['value-builtin', 'value-external'],
+                  'type-internal',
+                  'value-internal',
+                  ['type-parent', 'type-sibling', 'type-index'],
+                  ['value-parent', 'value-sibling', 'value-index'],
                   'unknown',
                 ],
                 customGroups: [],
diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts
index ebbb7d5..f4f4448 100644
--- a/rules/sort-imports.ts
+++ b/rules/sort-imports.ts
@@ -60,6 +60,16 @@ import { complete } from '../utils/complete'
  */
 let cachedGroupsByModifiersAndSelectors = new Map<string, string[]>()
 
+let defaultGroups = [
+  'type-import',
+  ['value-builtin', 'value-external'],
+  'type-internal',
+  'value-internal',
+  ['type-parent', 'type-sibling', 'type-index'],
+  ['value-parent', 'value-sibling', 'value-index'],
+  'unknown',
+]
+
 export type MESSAGE_ID =
   | 'missedSpacingBetweenImports'
   | 'unexpectedImportsGroupOrder'
@@ -73,15 +83,6 @@ export default createEslintRule<Options, MESSAGE_ID>({
     let userOptions = context.options.at(0)
     let options = getOptionsWithCleanGroups(
       complete(userOptions, settings, {
-        groups: [
-          'type',
-          ['builtin', 'external'],
-          'internal-type',
-          'internal',
-          ['parent-type', 'sibling-type', 'index-type'],
-          ['parent', 'sibling', 'index'],
-          'unknown',
-        ],
         fallbackSort: { type: 'unsorted' },
         internalPattern: ['^~/.+'],
         partitionByComment: false,
@@ -89,6 +90,7 @@ export default createEslintRule<Options, MESSAGE_ID>({
         newlinesBetween: 'always',
         specialCharacters: 'keep',
         sortSideEffects: false,
+        groups: defaultGroups,
         type: 'alphabetical',
         environment: 'node',
         customGroups: [],
@@ -230,6 +232,11 @@ export default createEslintRule<Options, MESSAGE_ID>({
       for (let selector of commonSelectors) {
         selectors.push(selector)
       }
+      selectors.push('import')
+
+      if (!modifiers.includes('type')) {
+        modifiers.push('value')
+      }
 
       group ??=
         computeGroupExceptUnknown({
@@ -475,15 +482,6 @@ export default createEslintRule<Options, MESSAGE_ID>({
   },
   defaultOptions: [
     {
-      groups: [
-        'type',
-        ['builtin', 'external'],
-        'internal-type',
-        'internal',
-        ['parent-type', 'sibling-type', 'index-type'],
-        ['parent', 'sibling', 'index'],
-        'unknown',
-      ],
       customGroups: { value: {}, type: {} },
       internalPattern: ['^~/.+'],
       partitionByComment: false,
@@ -491,6 +489,7 @@ export default createEslintRule<Options, MESSAGE_ID>({
       specialCharacters: 'keep',
       newlinesBetween: 'always',
       sortSideEffects: false,
+      groups: defaultGroups,
       type: 'alphabetical',
       environment: 'node',
       ignoreCase: true,
diff --git a/rules/sort-imports/types.ts b/rules/sort-imports/types.ts
index 9d19c92..d4570ce 100644
--- a/rules/sort-imports/types.ts
+++ b/rules/sort-imports/types.ts
@@ -59,6 +59,7 @@ export type Selector =
   | InternalSelector
   | BuiltinSelector
   | SiblingSelector
+  | ImportSelector
   | ObjectSelector
   | ParentSelector
   | IndexSelector
@@ -78,7 +79,7 @@ export interface SortImportsSortingNode extends SortingNode {
 
 export type Group = ValueGroup | TypeGroup | 'unknown' | string
 
-export type Modifier = TypeModifier
+export type Modifier = ValueModifier | TypeModifier
 
 type TypeGroup = JoinWithDash<[TypeModifier, Selector]>
 
@@ -133,10 +134,14 @@ type ParentSelector = 'parent'
  */
 type ObjectSelector = 'object'
 
+type ImportSelector = 'import'
+
 type IndexSelector = 'index'
 
 type StyleSelector = 'style'
 
+type ValueModifier = 'value'
+
 type TypeModifier = 'type'
 
 type TypeSelector = 'type'
@@ -148,6 +153,7 @@ export let allSelectors: Selector[] = [
   'internal',
   'builtin',
   'sibling',
+  'import',
   'parent',
   'index',
   'style',
@@ -164,7 +170,7 @@ export let allDeprecatedSelectors: Selector[] = [
   'object',
 ]
 
-export let allModifiers: Modifier[] = ['type']
+export let allModifiers: Modifier[] = ['type', 'value']
 
 /**
  * Ideally, we should generate as many schemas as there are selectors, and ensure


Test patch:
diff --git a/test/rules/sort-imports.test.ts b/test/rules/sort-imports.test.ts
index a6f54a4..50ee9d2 100644
--- a/test/rules/sort-imports.test.ts
+++ b/test/rules/sort-imports.test.ts
@@ -88,8 +88,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                rightGroup: 'internal-type',
-                leftGroup: 'internal',
+                rightGroup: 'type-internal',
+                leftGroup: 'value-internal',
                 right: '~/i',
                 left: '~/b',
               },
@@ -104,8 +104,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                leftGroup: 'sibling-type',
-                rightGroup: 'builtin',
+                rightGroup: 'value-builtin',
+                leftGroup: 'type-sibling',
                 left: './d',
                 right: 'fs',
               },
@@ -127,17 +127,17 @@ describe(ruleName, () => {
             },
             {
               data: {
-                rightGroup: 'index-type',
+                leftGroup: 'value-parent',
+                rightGroup: 'type-index',
                 right: './index.d.ts',
-                leftGroup: 'parent',
                 left: '../../h',
               },
               messageId: 'unexpectedImportsGroupOrder',
             },
             {
               data: {
-                leftGroup: 'index',
-                rightGroup: 'type',
+                rightGroup: 'type-import',
+                leftGroup: 'value-index',
                 right: 't',
                 left: '.',
               },
@@ -243,8 +243,8 @@ describe(ruleName, () => {
           errors: [
             {
               data: {
-                rightGroup: 'external',
-                leftGroup: 'index',
+                rightGroup: 'value-external',
+                leftGroup: 'value-index',
                 right: 'a',
                 left: '.',
               },
@@ -252,8 +252,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                leftGroup: 'internal',
-                rightGroup: 'type',
+                leftGroup: 'value-internal',
+                rightGroup: 'type-import',
                 left: '~/c',
                 right: 't',
               },
@@ -261,8 +261,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                rightGroup: 'internal',
-                leftGroup: 'parent',
+                rightGroup: 'value-internal',
+                leftGroup: 'value-parent',
                 left: '../../e',
                 right: '~/b',
               },
@@ -383,9 +383,9 @@ describe(ruleName, () => {
               },
               {
                 data: {
-                  rightGroup: 'external',
+                  rightGroup: 'value-external',
+                  leftGroup: 'value-parent',
                   right: 'console.log',
-                  leftGroup: 'parent',
                   left: '../b',
                 },
                 messageId: 'unexpectedImportsGroupOrder',
@@ -706,8 +706,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                rightGroup: 'external',
-                leftGroup: 'sibling',
+                rightGroup: 'value-external',
+                leftGroup: 'value-sibling',
                 left: './b',
                 right: 'c',
               },
@@ -1043,8 +1043,8 @@ describe(ruleName, () => {
               },
               {
                 data: {
-                  leftGroup: 'internal',
-                  rightGroup: 'builtin',
+                  leftGroup: 'value-internal',
+                  rightGroup: 'value-builtin',
                   left: '~/b',
                   right: 'fs',
                 },
@@ -2933,6 +2933,45 @@ describe(ruleName, () => {
             valid: [],
           },
         )
+
+        ruleTester.run(
+          `${ruleName}(${type}): prioritizes "external" over "import"`,
+          rule,
+          {
+            invalid: [
+              {
+                errors: [
+                  {
+                    data: {
+                      rightGroup: 'external',
+                      leftGroup: 'import',
+                      left: './a',
+                      right: 'b',
+                    },
+                    messageId: 'unexpectedImportsGroupOrder',
+                  },
+                ],
+                options: [
+                  {
+                    ...options,
+                    groups: ['external', 'import'],
+                  },
+                ],
+                output: dedent`
+                  import b from 'b'
+
+                  import a from './a'
+                `,
+                code: dedent`
+                  import a from './a'
+
+                  import b from 'b'
+                `,
+              },
+            ],
+            valid: [],
+          },
+        )
       })
 
       describe(`custom groups`, () => {
@@ -3378,8 +3417,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                rightGroup: 'internal-type',
-                leftGroup: 'internal',
+                rightGroup: 'type-internal',
+                leftGroup: 'value-internal',
                 right: '~/i',
                 left: '~/b',
               },
@@ -3394,8 +3433,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                leftGroup: 'sibling-type',
-                rightGroup: 'builtin',
+                rightGroup: 'value-builtin',
+                leftGroup: 'type-sibling',
                 left: './d',
                 right: 'fs',
               },
@@ -3417,17 +3456,17 @@ describe(ruleName, () => {
             },
             {
               data: {
-                rightGroup: 'index-type',
+                leftGroup: 'value-parent',
+                rightGroup: 'type-index',
                 right: './index.d.ts',
-                leftGroup: 'parent',
                 left: '../../h',
               },
               messageId: 'unexpectedImportsGroupOrder',
             },
             {
               data: {
-                leftGroup: 'index',
-                rightGroup: 'type',
+                rightGroup: 'type-import',
+                leftGroup: 'value-index',
                 right: 't',
                 left: '.',
               },
@@ -3533,8 +3572,8 @@ describe(ruleName, () => {
           errors: [
             {
               data: {
-                rightGroup: 'external',
-                leftGroup: 'index',
+                rightGroup: 'value-external',
+                leftGroup: 'value-index',
                 right: 'a',
                 left: '.',
               },
@@ -3542,8 +3581,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                leftGroup: 'internal',
-                rightGroup: 'type',
+                leftGroup: 'value-internal',
+                rightGroup: 'type-import',
                 left: '~/c',
                 right: 't',
               },
@@ -3551,8 +3590,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                rightGroup: 'internal',
-                leftGroup: 'parent',
+                rightGroup: 'value-internal',
+                leftGroup: 'value-parent',
                 left: '../../e',
                 right: '~/b',
               },
@@ -3673,9 +3712,9 @@ describe(ruleName, () => {
               },
               {
                 data: {
-                  rightGroup: 'external',
+                  rightGroup: 'value-external',
+                  leftGroup: 'value-parent',
                   right: 'console.log',
-                  leftGroup: 'parent',
                   left: '../b',
                 },
                 messageId: 'unexpectedImportsGroupOrder',
@@ -3997,8 +4036,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                rightGroup: 'external',
-                leftGroup: 'sibling',
+                rightGroup: 'value-external',
+                leftGroup: 'value-sibling',
                 left: './b',
                 right: 'c',
               },
@@ -4334,8 +4373,8 @@ describe(ruleName, () => {
               },
               {
                 data: {
-                  leftGroup: 'internal',
-                  rightGroup: 'builtin',
+                  leftGroup: 'value-internal',
+                  rightGroup: 'value-builtin',
                   left: '~/b',
                   right: 'fs',
                 },
@@ -4699,8 +4738,8 @@ describe(ruleName, () => {
           errors: [
             {
               data: {
-                rightGroup: 'internal-type',
-                leftGroup: 'internal',
+                rightGroup: 'type-internal',
+                leftGroup: 'value-internal',
                 right: '~/i',
                 left: '~/b',
               },
@@ -4715,8 +4754,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                leftGroup: 'sibling-type',
-                rightGroup: 'builtin',
+                rightGroup: 'value-builtin',
+                leftGroup: 'type-sibling',
                 left: './d',
                 right: 'fs',
               },
@@ -4752,17 +4791,17 @@ describe(ruleName, () => {
             },
             {
               data: {
-                rightGroup: 'index-type',
+                leftGroup: 'value-parent',
+                rightGroup: 'type-index',
                 right: './index.d.ts',
-                leftGroup: 'parent',
                 left: '../../h',
               },
               messageId: 'unexpectedImportsGroupOrder',
             },
             {
               data: {
-                leftGroup: 'index',
-                rightGroup: 'type',
+                rightGroup: 'type-import',
+                leftGroup: 'value-index',
                 right: 't',
                 left: '.',
               },
@@ -4882,8 +4921,8 @@ describe(ruleName, () => {
           errors: [
             {
               data: {
-                rightGroup: 'external',
-                leftGroup: 'index',
+                rightGroup: 'value-external',
+                leftGroup: 'value-index',
                 right: 'a',
                 left: '.',
               },
@@ -4891,8 +4930,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                leftGroup: 'internal',
-                rightGroup: 'type',
+                leftGroup: 'value-internal',
+                rightGroup: 'type-import',
                 left: '~/c',
                 right: 't',
               },
@@ -4900,8 +4939,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                rightGroup: 'internal',
-                leftGroup: 'parent',
+                rightGroup: 'value-internal',
+                leftGroup: 'value-parent',
                 left: '../../e',
                 right: '~/b',
               },
@@ -5022,9 +5061,9 @@ describe(ruleName, () => {
               },
               {
                 data: {
-                  rightGroup: 'external',
+                  rightGroup: 'value-external',
+                  leftGroup: 'value-parent',
                   right: 'console.log',
-                  leftGroup: 'parent',
                   left: '../b',
                 },
                 messageId: 'unexpectedImportsGroupOrder',
@@ -5331,8 +5370,8 @@ describe(ruleName, () => {
             },
             {
               data: {
-                rightGroup: 'external',
-                leftGroup: 'sibling',
+                rightGroup: 'value-external',
+                leftGroup: 'value-sibling',
                 left: './b',
                 right: 'c',
               },
@@ -5726,8 +5765,8 @@ describe(ruleName, () => {
             errors: [
               {
                 data: {
-                  leftGroup: 'internal',
-                  rightGroup: 'builtin',
+                  leftGroup: 'value-internal',
+                  rightGroup: 'value-builtin',
                   left: '~/b',
                   right: 'fs',
                 },