Repository: azat-io/eslint-plugin-perfectionist
Pull request #512 (2025-04-12 13:49:33)

feat(sort-imports): add `subpath`, `tsconfig-path` selectors and 5 modifiers
- Follow-up of https://github.com/azat-io/eslint-plugin-perfectionist/pull/511.
- Resolves https://github.com/azat-io/eslint-plugin-perfectionist/issues/510.
- Resolves https://github.com/azat-io/eslint-plugin-perfectionist/issues/494.
- Resolves https://github.com/azat-io/eslint-plugin-perfectionist/issues/515.

# Description

This PR adds the specific selectors and modifiers to `sort-import`:

## Selectors

- `subpath` (https://github.com/azat-io/eslint-plugin-perfectionist/issues/510)
  - The priority of this selector is right below `parent`. 
- `tsconfig-path` (https://github.com/azat-io/eslint-plugin-perfectionist/issues/515)
  - The priority of this selector is right below `style`.


## Modifiers

- `side-effect`: Side-effect imports.
- `require`: Imports that use `require`.
- `default` (https://github.com/azat-io/eslint-plugin-perfectionist/issues/494): Imports using the `default` specifier. 
- `wildcard`: Imports using the `* as` specifier.
- `named` (https://github.com/azat-io/eslint-plugin-perfectionist/issues/494): Imports having a named specifier.

The final ordered list of modifiers is:

`side-effect`, `type`, `value`, `require`, `default`, `wildcard`, `named`.

### Example 1

```ts
import defaultSpecifier, { readFile } from "node:fs"
```

Will match (ordered):
- `value-default-named-external`
- `value-default-external`
- `value-named-external`
- `default-named-external`
- `value-external`
- `default-external`
- `named-external`
- `external`
- `value-default-named-import`
- `value-default-import`
- `value-named-import`
- `default-named-import`
- `value-import`
- `default-import`
- `named-import`
- `import`

### Example 2

```ts
import 'style.scss'
```

Will match (ordered):

- `side-effect-style` (`side-effect-style` selector).
- `side-effect`.
- `side-effect-style` (`style` selector, `side-effect` modifier).
- `style`.
- `side-effect-import`.
- `value-import`
- `import`

Side-effect imports are necessarily non-`type`, so I haven't included `value-side-effect`-like matches (but they work).

### Example 3

```ts
import 'side-effect-import'
```

Will match (ordered):

- `side-effect`.
- `side-effect-external`.
- `value-external`.
- `external`.
- `side-effect-import`.
- `value-import`.
- `import`.

Side-effect imports are necessarily non-`type`, so I haven't included `value-side-effect`-like matches (but they work).

### 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/512?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 [(`223d340`)](https://app.codecov.io/gh/azat-io/eslint-plugin-perfectionist/commit/223d3407dee414876a2529c410bd25ad27336093?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      #512    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           96        97     +1     
  Lines         7743      7875   +132     
  Branches      1556      1587    +31     
==========================================
+ Hits          7743      7875   +132     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/azat-io/eslint-plugin-perfectionist/pull/512?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: I want to create a new release. Do you think I should wait for this PR?
hugop95: @azat-io 
This PR is almost done. Tests need to be added, but it shouldn't take long. In my opinion, it's better to release all these sort-import features together.
azat-io: Great. Thanks, I'll wait!

Patch:
diff --git a/docs/content/rules/sort-imports.mdx b/docs/content/rules/sort-imports.mdx
index 0aba116..de4967d 100644
--- a/docs/content/rules/sort-imports.mdx
+++ b/docs/content/rules/sort-imports.mdx
@@ -271,7 +271,9 @@ This option is only available when the type is set to `'line-length'`.
 
 <sub>default: `undefined`</sub>
 
-Specifies the  directory of the root `tsconfig.json` file (ex: `.`). This is used for marking aliased imports as `internal` or `internal-type` in the `groups` option.
+Specifies the  directory of the root `tsconfig.json` file (ex: `.`). This is used in [`groups`](#groups) for:
+- Marking aliased imports as `internal`.
+- Marking imports matching [tsconfig paths](https://www.typescriptlang.org/tsconfig/#paths) as `tsconfig-path`.
 
 ### groups
 
@@ -314,9 +316,11 @@ The list of selectors is sorted from most to least important:
 - `'side-effect-style'` — Side effect style imports.
 - `'side-effect'` — Side effect imports.
 - `'style'` — Styles.
+- `'tsconfig-path'` — `tsconfig` [paths](https://www.typescriptlang.org/tsconfig/#paths) alias imports. Requires [`tsconfigRootDir`](#tsconfigrootdir) to be set.
 - `'index'` — Main file from the current directory.
 - `'sibling'` — Modules from the same directory.
 - `'parent'` — Modules from the parent directory.
+- `'subpath'` — Node.js [subpath](https://nodejs.org/api/packages.html#packages_subpath_imports) imports.
 - `'internal'` — Your internal modules.
 - `'builtin'` — Node.js Built-in Modules.
 - `'external'` — External modules installed in the project.
@@ -324,9 +328,16 @@ The list of selectors is sorted from most to least important:
 
 #### Modifiers
 
-- `'type'` — Typescript time imports.
+The list of modifiers is sorted from most to least important:
+
+- `'side-effect'` — Side effect imports.
+- `'type'` — Typescript type imports.
 - `'value'` — Value imports.
 - `'ts-equals'` — TypeScript import-equals imports.
+- `'require'` — `require` imports.
+- `'default'` — Imports containing the `default` specifier.
+- `'wildcard'` — Imports containing the wildcard (`* as`) specifier.
+- `'named'` — Imports containing at least one named specifier.
 
 #### Important notes
 
@@ -342,6 +353,7 @@ In case of multiple groups matching an element, the following rules will be appl
 
 1. Selector priority: `type`, `index`, ... will take precedence over `external` groups for example.
 2. If the selector is the same, the group with the most modifiers matching will be selected.
+3. If modifiers quantity is the same, order will be chosen based on modifier importance as listed above.
 
 Example 1:
 
@@ -350,13 +362,34 @@ import type { FC } from 'react'
 ```
 
 `react` can be matched by the following groups, from most to least important:
-- `type` (`type` selector).
-- `type-external` (`type` modifier).
+- `named-type` (`type` selector).
+- `type`.
+- `named-type-external` or `type-named-external` (`type` modifier).
+- `type-external`.
+- `named-external`.
 - `external`.
+- `named-type-import` or `type-named-import`.
 - `type-import`.
+- `named-import`.
+- `import`.
+
+Example 2:
+
+```ts
+import 'style.scss'
+```
+
+`style.scss` can be matched by the following relevant groups, from most to least important:
+
+- `side-effect-style` (`side-effect-style` selector).
+- `side-effect`.
+- `value-style`.
+- `style`.
+- `side-effect-import`.  (`side-effect` modifier)
+- `value-import`.
 - `import`.
 
-Example 2 (The most important group is written in the comments):
+Example 3 (The most important group is written in the comments):
 
 ```ts
 // 'value-builtin' - Node.js Built-in Modules
@@ -367,6 +400,8 @@ import axios from 'axios'
 import Button from '~/components/Button'
 // 'value-parent' - Modules from parent directory
 import formatNumber from '../utils/format-number'
+// 'value-subpath' - Node.js subpath imports
+import subpathContent from '#subpathModule'
 // 'value-sibling' - Modules from the same directory
 import config from './config'
 // 'value-side-effect' - Side effect imports
@@ -379,15 +414,15 @@ import main from '.'
 import styles from './index.module.css'
 // 'type-external' - TypeScript type imports
 import type { FC } from 'react'
-// 'type-builtin' - TypeScript type imports from Built-in Modules
+// 'named-type-builtin' - TypeScript type imports from Built-in Modules
 import type { Server } from 'http'
-// 'type-internal' - TypeScript type imports from your internal modules
+// 'named-type-internal' - TypeScript type imports from your internal modules
 import type { User } from '~/users'
-// 'type-parent' - TypeScript type imports from parent directory
+// 'named-type-parent' - TypeScript type imports from parent directory
 import type { InputProps } from '../Input'
-// 'type-sibling' - TypeScript type imports from the same directory
+// 'named-type-sibling' - TypeScript type imports from the same directory
 import type { Details } from './data'
-// 'type-index' - TypeScript type imports from main directory file
+// 'named-type-index' - TypeScript type imports from main directory file
 import type { BaseOptions } from './index.d.ts'
 // 'value-ts-equals-import' - TypeScript import-equals imports
 import NotFoundError = ErrorsNamespace.NotFoundError
diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts
index 0a72853..8c4c1df 100644
--- a/rules/sort-imports.ts
+++ b/rules/sort-imports.ts
@@ -190,7 +190,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
           }
 
           for (let selector of commonSelectors) {
-            selectors.push(`${selector}-type`)
+            if (selector !== 'subpath' && selector !== 'tsconfig-path') {
+              selectors.push(`${selector}-type`)
+            }
           }
         }
 
@@ -228,6 +230,7 @@ export default createEslintRule<Options, MESSAGE_ID>({
 
         if (isSideEffect) {
           selectors.push('side-effect')
+          modifiers.push('side-effect')
         }
 
         if (isStyleValue) {
@@ -248,6 +251,22 @@ export default createEslintRule<Options, MESSAGE_ID>({
         modifiers.push('ts-equals')
       }
 
+      if (node.type === 'VariableDeclaration') {
+        modifiers.push('require')
+      }
+
+      if (hasSpecifier(node, 'ImportDefaultSpecifier')) {
+        modifiers.push('default')
+      }
+
+      if (hasSpecifier(node, 'ImportNamespaceSpecifier')) {
+        modifiers.push('wildcard')
+      }
+
+      if (hasSpecifier(node, 'ImportSpecifier')) {
+        modifiers.push('named')
+      }
+
       group ??=
         computeGroupExceptUnknown({
           customGroups: Array.isArray(options.customGroups)
@@ -544,6 +563,19 @@ let hasContentBetweenNodes = (
     includeComments: false,
   }).length > 0
 
+let hasSpecifier = (
+  node:
+    | TSESTree.TSImportEqualsDeclaration
+    | TSESTree.VariableDeclaration
+    | TSESTree.ImportDeclaration,
+  specifier:
+    | 'ImportNamespaceSpecifier'
+    | 'ImportDefaultSpecifier'
+    | 'ImportSpecifier',
+): boolean =>
+  node.type === 'ImportDeclaration' &&
+  node.specifiers.some(nodeSpecifier => nodeSpecifier.type === specifier)
+
 let styleExtensions = [
   '.less',
   '.scss',
@@ -562,8 +594,11 @@ let isSideEffectImport = ({
   sourceCode,
   node,
 }: {
+  node:
+    | TSESTree.TSImportEqualsDeclaration
+    | TSESTree.VariableDeclaration
+    | TSESTree.ImportDeclaration
   sourceCode: TSESLint.SourceCode
-  node: TSESTree.Node
 }): boolean =>
   node.type === 'ImportDeclaration' &&
   node.specifiers.length === 0 &&
diff --git a/rules/sort-imports/compute-common-selectors.ts b/rules/sort-imports/compute-common-selectors.ts
index cceb04c..fa0b095 100644
--- a/rules/sort-imports/compute-common-selectors.ts
+++ b/rules/sort-imports/compute-common-selectors.ts
@@ -4,12 +4,20 @@ import type { ReadClosestTsConfigByPathValue } from './read-closest-ts-config-by
 import type { RegexOption } from '../../types/common-options'
 import type { Selector } from './types'
 
+import { matchesTsconfigPaths } from './matches-tsconfig-paths'
 import { getTypescriptImport } from './get-typescript-import'
 import { matches } from '../../utils/matches'
 
 type CommonSelector = Extract<
   Selector,
-  'internal' | 'external' | 'sibling' | 'builtin' | 'parent' | 'index'
+  | 'tsconfig-path'
+  | 'internal'
+  | 'external'
+  | 'sibling'
+  | 'builtin'
+  | 'subpath'
+  | 'parent'
+  | 'index'
 >
 
 export let computeCommonSelectors = ({
@@ -37,33 +45,47 @@ export let computeCommonSelectors = ({
         name,
       })
 
-  let predefinedGroups: CommonSelector[] = []
+  let commonSelectors: CommonSelector[] = []
+
+  if (
+    tsConfigOutput &&
+    matchesTsconfigPaths({
+      tsConfigOutput,
+      name,
+    })
+  ) {
+    commonSelectors.push('tsconfig-path')
+  }
 
   if (isIndex(name)) {
-    predefinedGroups.push('index')
+    commonSelectors.push('index')
   }
 
   if (isSibling(name)) {
-    predefinedGroups.push('sibling')
+    commonSelectors.push('sibling')
   }
 
   if (isParent(name)) {
-    predefinedGroups.push('parent')
+    commonSelectors.push('parent')
+  }
+
+  if (isSubpath(name)) {
+    commonSelectors.push('subpath')
   }
 
   if (internalExternalGroup === 'internal') {
-    predefinedGroups.push('internal')
+    commonSelectors.push('internal')
   }
 
   if (isCoreModule(name, options.environment)) {
-    predefinedGroups.push('builtin')
+    commonSelectors.push('builtin')
   }
 
   if (internalExternalGroup === 'external') {
-    predefinedGroups.push('external')
+    commonSelectors.push('external')
   }
 
-  return predefinedGroups
+  return commonSelectors
 }
 
 let bunModules = new Set([
@@ -92,6 +114,8 @@ let isParent = (value: string): boolean => value.startsWith('..')
 
 let isSibling = (value: string): boolean => value.startsWith('./')
 
+let isSubpath = (value: string): boolean => value.startsWith('#')
+
 let isIndex = (value: string): boolean =>
   [
     './index.d.js',
diff --git a/rules/sort-imports/matches-tsconfig-paths.ts b/rules/sort-imports/matches-tsconfig-paths.ts
new file mode 100644
index 0000000..710334b
--- /dev/null
+++ b/rules/sort-imports/matches-tsconfig-paths.ts
@@ -0,0 +1,32 @@
+import type { ReadClosestTsConfigByPathValue } from './read-closest-ts-config-by-path'
+
+let regexByTsconfigPathCache = new Map<string, RegExp>()
+
+export let matchesTsconfigPaths = ({
+  tsConfigOutput,
+  name,
+}: {
+  tsConfigOutput: ReadClosestTsConfigByPathValue
+  name: string
+}): boolean => {
+  if (!tsConfigOutput.compilerOptions.paths) {
+    return false
+  }
+
+  return Object.keys(tsConfigOutput.compilerOptions.paths).some(key =>
+    getRegexByTsconfigPath(key).test(name),
+  )
+}
+
+let getRegexByTsconfigPath = (path: string): RegExp => {
+  let existingRegex = regexByTsconfigPathCache.get(path)
+  if (existingRegex) {
+    return existingRegex
+  }
+  let regex = new RegExp(`^${escapeRegExp(path).replaceAll('*', '(.+)')}$`)
+  regexByTsconfigPathCache.set(path, regex)
+  return regex
+}
+
+let escapeRegExp = (value: string): string =>
+  value.replaceAll(/[$+.?[\\\]]/gu, String.raw`\$&`)
diff --git a/rules/sort-imports/types.ts b/rules/sort-imports/types.ts
index 8bad72f..fc1d0e5 100644
--- a/rules/sort-imports/types.ts
+++ b/rules/sort-imports/types.ts
@@ -50,6 +50,7 @@ export type Selector =
   | SideEffectStyleSelector
   | InternalTypeSelector
   | ExternalTypeSelector
+  | TsconfigPathSelector
   | SiblingTypeSelector
   | BuiltinTypeSelector
   | SideEffectSelector
@@ -59,6 +60,7 @@ export type Selector =
   | InternalSelector
   | BuiltinSelector
   | SiblingSelector
+  | SubpathSelector
   | ImportSelector
   | ObjectSelector
   | ParentSelector
@@ -66,6 +68,16 @@ export type Selector =
   | StyleSelector
   | TypeSelector
 
+export type Modifier =
+  | SideEffectModifier
+  | WildcardModifier
+  | TsEqualsModifier
+  | RequireModifier
+  | DefaultModifier
+  | ValueModifier
+  | NamedModifier
+  | TypeModifier
+
 export type SingleCustomGroup = {
   modifiers?: Modifier[]
   selector?: Selector
@@ -77,8 +89,6 @@ export interface SortImportsSortingNode extends SortingNodeWithDependencies {
   isIgnored: boolean
 }
 
-export type Modifier = TsEqualsModifier | ValueModifier | TypeModifier
-
 export type Group = ValueGroup | TypeGroup | 'unknown' | string
 
 type TypeGroup = JoinWithDash<[TypeModifier, Selector]>
@@ -95,6 +105,8 @@ type InternalTypeSelector = 'internal-type'
  */
 type ExternalTypeSelector = 'external-type'
 
+type TsconfigPathSelector = 'tsconfig-path'
+
 type ValueGroup = JoinWithDash<[Selector]>
 
 /**
@@ -114,6 +126,8 @@ type SideEffectSelector = 'side-effect'
  */
 type ParentTypeSelector = 'parent-type'
 
+type SideEffectModifier = 'side-effect'
+
 /**
  * @deprecated for the modifier and selector
  */
@@ -121,14 +135,22 @@ type IndexTypeSelector = 'index-type'
 
 type TsEqualsModifier = 'ts-equals'
 
+type WildcardModifier = 'wildcard'
+
 type ExternalSelector = 'external'
 
 type InternalSelector = 'internal'
 
+type SubpathSelector = 'subpath'
+
 type BuiltinSelector = 'builtin'
 
 type SiblingSelector = 'sibling'
 
+type DefaultModifier = 'default'
+
+type RequireModifier = 'require'
+
 type ParentSelector = 'parent'
 
 /**
@@ -144,17 +166,21 @@ type StyleSelector = 'style'
 
 type ValueModifier = 'value'
 
+type NamedModifier = 'named'
+
 type TypeModifier = 'type'
 
 type TypeSelector = 'type'
 
 export let allSelectors: Selector[] = [
   'side-effect-style',
+  'tsconfig-path',
   'side-effect',
   'external',
   'internal',
   'builtin',
   'sibling',
+  'subpath',
   'import',
   'parent',
   'index',
@@ -172,7 +198,16 @@ export let allDeprecatedSelectors: Selector[] = [
   'object',
 ]
 
-export let allModifiers: Modifier[] = ['type', 'ts-equals', 'value']
+export let allModifiers: Modifier[] = [
+  'default',
+  'named',
+  'require',
+  'side-effect',
+  'ts-equals',
+  'type',
+  'value',
+  'wildcard',
+]
 
 /**
  * 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 7c5578c..e283fcc 100644
--- a/test/rules/sort-imports.test.ts
+++ b/test/rules/sort-imports.test.ts
@@ -2880,29 +2880,20 @@ describe(ruleName, () => {
                 options: [
                   {
                     ...options,
-
                     groups: [
                       'style',
                       [
                         'index',
                         'internal',
+                        'subpath',
                         'external',
                         'sibling',
                         'builtin',
                         'parent',
+                        'tsconfig-path',
                       ],
                     ],
-                  },
-                ],
-                errors: [
-                  {
-                    data: {
-                      leftGroup: 'builtin',
-                      rightGroup: 'style',
-                      right: 'style.css',
-                      left: 'timers',
-                    },
-                    messageId: 'unexpectedImportsGroupOrder',
+                    tsconfigRootDir: '.',
                   },
                 ],
                 output: dedent`
@@ -2911,6 +2902,8 @@ describe(ruleName, () => {
                   import a from '../a'
                   import b from './b'
                   import c from './index'
+                  import subpath from '#subpath'
+                  import tsConfigPath from '$path'
                   import d from 'd'
                   import e from 'timers'
                 `,
@@ -2918,11 +2911,31 @@ describe(ruleName, () => {
                   import a from '../a'
                   import b from './b'
                   import c from './index'
+                  import subpath from '#subpath'
+                  import tsConfigPath from '$path'
                   import d from 'd'
                   import e from 'timers'
 
                   import style from 'style.css'
                 `,
+                errors: [
+                  {
+                    data: {
+                      leftGroup: 'builtin',
+                      rightGroup: 'style',
+                      right: 'style.css',
+                      left: 'timers',
+                    },
+                    messageId: 'unexpectedImportsGroupOrder',
+                  },
+                ],
+                before: () => {
+                  mockReadClosestTsConfigByPathWith({
+                    paths: {
+                      $path: ['./path'],
+                    },
+                  })
+                },
               },
             ],
             valid: [],
@@ -2994,14 +3007,11 @@ describe(ruleName, () => {
                   },
                 ],
                 output: dedent`
-                  import type a from './a'
                   import type z = z
 
                   import f from 'f'
                 `,
                 code: dedent`
-                  import type a from './a'
-
                   import f from 'f'
 
                   import type z = z
@@ -3012,6 +3022,46 @@ describe(ruleName, () => {
           },
         )
 
+        ruleTester.run(
+          `${ruleName}(${type}): prioritizes "side-effect" over "value"`,
+          rule,
+          {
+            invalid: [
+              {
+                errors: [
+                  {
+                    data: {
+                      rightGroup: 'side-effect-import',
+                      leftGroup: 'external',
+                      right: './z',
+                      left: 'f',
+                    },
+                    messageId: 'unexpectedImportsGroupOrder',
+                  },
+                ],
+                options: [
+                  {
+                    ...options,
+                    groups: ['side-effect-import', 'external', 'value-import'],
+                    sortSideEffects: true,
+                  },
+                ],
+                output: dedent`
+                  import "./z"
+
+                  import f from 'f'
+                `,
+                code: dedent`
+                  import f from 'f'
+
+                  import "./z"
+                `,
+              },
+            ],
+            valid: [],
+          },
+        )
+
         ruleTester.run(
           `${ruleName}(${type}): prioritizes "value" over "ts-equals"`,
           rule,
@@ -3036,14 +3086,11 @@ describe(ruleName, () => {
                   },
                 ],
                 output: dedent`
-                  import a from './a'
                   import z = z
 
                   import f from 'f'
                 `,
                 code: dedent`
-                  import a from './a'
-
                   import f from 'f'
 
                   import z = z
@@ -3053,6 +3100,123 @@ describe(ruleName, () => {
             valid: [],
           },
         )
+
+        ruleTester.run(
+          `${ruleName}(${type}): prioritizes "ts-equals" over "require"`,
+          rule,
+          {
+            invalid: [
+              {
+                errors: [
+                  {
+                    data: {
+                      rightGroup: 'ts-equals-import',
+                      leftGroup: 'external',
+                      right: './z',
+                      left: 'f',
+                    },
+                    messageId: 'unexpectedImportsGroupOrder',
+                  },
+                ],
+                options: [
+                  {
+                    ...options,
+                    groups: ['ts-equals-import', 'external', 'require-import'],
+                  },
+                ],
+                output: dedent`
+                  import z = require('./z')
+
+                  import f from 'f'
+                `,
+                code: dedent`
+                  import f from 'f'
+
+                  import z = require('./z')
+                `,
+              },
+            ],
+            valid: [],
+          },
+        )
+
+        ruleTester.run(
+          `${ruleName}(${type}): prioritizes "default" over "named"`,
+          rule,
+          {
+            invalid: [
+              {
+                errors: [
+                  {
+                    data: {
+                      rightGroup: 'default-import',
+                      leftGroup: 'external',
+                      right: './z',
+                      left: 'f',
+                    },
+                    messageId: 'unexpectedImportsGroupOrder',
+                  },
+                ],
+                options: [
+                  {
+                    ...options,
+                    groups: ['default-import', 'external', 'named-import'],
+                  },
+                ],
+                output: dedent`
+                  import z, { z } from "./z"
+
+                  import f from 'f'
+                `,
+                code: dedent`
+                  import f from 'f'
+
+                  import z, { z } from "./z"
+                `,
+              },
+            ],
+            valid: [],
+          },
+        )
+
+        ruleTester.run(
+          `${ruleName}(${type}): prioritizes "wildcard" over "named"`,
+          rule,
+          {
+            invalid: [
+              {
+                errors: [
+                  {
+                    data: {
+                      rightGroup: 'wildcard-import',
+                      leftGroup: 'external',
+                      right: './z',
+                      left: 'f',
+                    },
+                    messageId: 'unexpectedImportsGroupOrder',
+                  },
+                ],
+                options: [
+                  {
+                    ...options,
+                    groups: ['wildcard-import', 'external', 'named-import'],
+                  },
+                ],
+                output: dedent`
+                  import z, * as z from "./z"
+
+                  import f from 'f'
+                `,
+                code: dedent`
+                  import f from 'f'
+
+                  import z, * as z from "./z"
+                `,
+              },
+            ],
+            valid: [],
+          },
+        )
       })
 
       describe(`custom groups`, () => {
@@ -7267,26 +7431,6 @@ describe(ruleName, () => {
           invalid: [],
         },
       )
-
-      let mockReadClosestTsConfigByPathWith = (
-        compilerOptions: CompilerOptions | null,
-      ): void => {
-        vi.spyOn(
-          readClosestTsConfigUtilities,
-          'readClosestTsConfigByPath',
-        ).mockReturnValue(
-          compilerOptions
-            ? {
-                cache: createModuleResolutionCache(
-                  '.',
-                  filename => filename,
-                  compilerOptions,
-                ),
-                compilerOptions,
-              }
-            : null,
-        )
-      }
     })
 
     let eslintDisableRuleTesterName = `${ruleName}: supports 'eslint-disable' for individual nodes`
@@ -7609,4 +7753,24 @@ describe(ruleName, () => {
       },
     )
   })
+
+  let mockReadClosestTsConfigByPathWith = (
+    compilerOptions: CompilerOptions | null,
+  ): void => {
+    vi.spyOn(
+      readClosestTsConfigUtilities,
+      'readClosestTsConfigByPath',
+    ).mockReturnValue(
+      compilerOptions
+        ? {
+            cache: createModuleResolutionCache(
+              '.',
+              filename => filename,
+              compilerOptions,
+            ),
+            compilerOptions,
+          }
+        : null,
+    )
+  }
 })
diff --git a/test/rules/sort-imports/compute-common-selector.test.ts b/test/rules/sort-imports/compute-common-selector.test.ts
index 03748f8..6e5b82c 100644
--- a/test/rules/sort-imports/compute-common-selector.test.ts
+++ b/test/rules/sort-imports/compute-common-selector.test.ts
@@ -15,6 +15,21 @@ vi.mock('../../../rules/sort-imports/get-typescript-import', () => ({
 }))
 
 describe('compute-common-selector', () => {
+  describe('`tsconfig-path` selector', () => {
+    it.each(['foo/a', 'foo/a/b'])("should match with '%s'", name => {
+      expect(
+        computeCommonSelectors(
+          buildParameters({
+            tsConfig: {
+              paths: ['foo/*'],
+            },
+            name,
+          }),
+        ),
+      ).toContain('tsconfig-path')
+    })
+  })
+
   describe('`index` selector', () => {
     it.each([
       './index.d.js',
@@ -61,6 +76,14 @@ describe('compute-common-selector', () => {
     })
   })
 
+  describe('`subpath` selector', () => {
+    it.each(['#', '#a', '#node:sqlite'])("should match with '%s'", name => {
+      expect(computeCommonSelectors(buildParameters({ name }))).toContain(
+        'subpath',
+      )
+    })
+  })
+
   describe('`builtin` selector', () => {
     describe('node builtin modules', () => {
       it.each([
@@ -115,9 +138,7 @@ describe('compute-common-selector', () => {
       })
 
       expect(
-        computeCommonSelectors(
-          buildParameters({ withTsConfigOutput: true, name: 'foo' }),
-        ),
+        computeCommonSelectors(buildParameters({ tsConfig: {}, name: 'foo' })),
       ).toContain('internal')
     })
   })
@@ -145,9 +166,7 @@ describe('compute-common-selector', () => {
       })
 
       expect(
-        computeCommonSelectors(
-          buildParameters({ withTsConfigOutput: true, name: 'foo' }),
-        ),
+        computeCommonSelectors(buildParameters({ tsConfig: {}, name: 'foo' })),
       ).toContain('external')
     })
 
@@ -158,9 +177,7 @@ describe('compute-common-selector', () => {
       })
 
       expect(
-        computeCommonSelectors(
-          buildParameters({ withTsConfigOutput: true, name: 'foo' }),
-        ),
+        computeCommonSelectors(buildParameters({ tsConfig: {}, name: 'foo' })),
       ).toContain('external')
     })
   })
@@ -185,18 +202,26 @@ describe('compute-common-selector', () => {
   })
 
   let buildParameters = ({
-    withTsConfigOutput,
     internalPattern,
     environment,
+    tsConfig,
     name,
   }: {
-    withTsConfigOutput?: boolean
+    tsConfig?: {
+      paths?: string[]
+    }
     environment?: 'node' | 'bun'
     internalPattern?: string[]
     name: string
   }): Parameters<typeof computeCommonSelectors>[0] => ({
-    tsConfigOutput: withTsConfigOutput
-      ? ({ compilerOptions: {} } as ReadClosestTsConfigByPathValue)
+    tsConfigOutput: tsConfig
+      ? ({
+          compilerOptions: {
+            paths: Object.fromEntries(
+              (tsConfig.paths ?? []).map(path => [path, ['*']]),
+            ),
+          },
+        } as ReadClosestTsConfigByPathValue)
       : null,
     options: {
       internalPattern: internalPattern ?? [],
diff --git a/test/rules/sort-imports/matches-tsconfig-paths.test.ts b/test/rules/sort-imports/matches-tsconfig-paths.test.ts
new file mode 100644
index 0000000..9233803
--- /dev/null
+++ b/test/rules/sort-imports/matches-tsconfig-paths.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it } from 'vitest'
+
+import type { ReadClosestTsConfigByPathValue } from '../../../rules/sort-imports/read-closest-ts-config-by-path'
+
+import { matchesTsconfigPaths } from '../../../rules/sort-imports/matches-tsconfig-paths'
+
+describe('matches-tsconfig-paths', () => {
+  it.each([
+    { paths: ['foo/*'], name: 'foo/a/b' },
+    { paths: ['*/foo'], name: 'a/b/foo' },
+    { name: 'a/b/foo/c/d', paths: ['*/foo/*'] },
+    { paths: ['$foo/*'], name: '$foo/a/b' },
+  ])('should match with (%s)', ({ paths, name }) => {
+    expect(
+      matchesTsconfigPaths({
+        tsConfigOutput: createTsConfig(paths),
+        name,
+      }),
+    ).toBeTruthy()
+  })
+
+  it.each([
+    { paths: ['foo/*'], name: 'foo/' },
+    { paths: ['a/foo'], name: '*/*/foo' },
+    { paths: ['foo/a/b'], name: 'foo/*/*' },
+  ])('should not match with (%s)', ({ paths, name }) => {
+    expect(
+      matchesTsconfigPaths({
+        tsConfigOutput: createTsConfig(paths),
+        name,
+      }),
+    ).toBeFalsy()
+  })
+
+  let createTsConfig = (paths: string[]): ReadClosestTsConfigByPathValue =>
+    ({
+      compilerOptions: {
+        paths: Object.fromEntries(paths.map(path => [path, ['*']])),
+      },
+    }) as ReadClosestTsConfigByPathValue
+})