diff --git a/.changeset/tough-glasses-end.md b/.changeset/tough-glasses-end.md
new file mode 100644
index 0000000..7655405
--- /dev/null
+++ b/.changeset/tough-glasses-end.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-import-sorting': minor
+---
+
+Add new `specifier-order` rule for sorting named imports
diff --git a/README.md b/README.md
index 0904675..07ae6b1 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
-## eslint-plugin-import-sorting
+# eslint-plugin-import-sorting
-Enforce a convention in the order of `import` statements, inspired by [isort](https://timothycrosley.github.io/isort/#how-does-isort-work)’s grouping style:
+Enforce a convention in the order of `import` statements, inspired by
+[isort](https://timothycrosley.github.io/isort/#how-does-isort-work)’s grouping style:
1. Node standard modules
2. Framework modules
@@ -8,17 +9,21 @@ Enforce a convention in the order of `import` statements, inspired by [isort](ht
4. Internal modules
5. Explicitly local modules
-This plugin includes an additional group for “style” imports where the import source ends in `.css` or other style format. Imports are sorted alphabetically, except for local modules, which are sorted by the number of `.` segements in the path first, then alphabetically.
+This plugin includes an additional group for “style” imports where the import
+source ends in `.css` or other style format. Imports are sorted alphabetically,
+except for local modules, which are sorted by the number of `.` segements in
+the path first, then alphabetically.
## Usage
-Install the plugin, and ESLint if is not already.
+Install the plugin, and ESLint if it is not already.
```sh
npm install --save-dev eslint eslint-plugin-import-sorting
```
-Include the plugin in the `plugins` key of your ESLint config and enable the rule.
+Include the plugin in the `plugins` key of your ESLint config and enable the
+rules.
```js
// eslint.config.js
@@ -31,26 +36,19 @@ export default [
'import-sorting': importSortingPlugin,
},
rules: {
- 'import-sorting/order': 'warn',
+ 'import-sorting/order': 'error',
},
},
]
```
-
- Legacy config example
+
-```js
-// .eslintrc.js
-
-module.exports = {
- plugins: ['import-sorting'],
- rules: {
- 'import-sorting/order': 'warn',
- },
-}
-```
+🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
-
+| Name | Description | 🔧 |
+| :----------------------------------------------- | :------------------------------------------ | :- |
+| [order](docs/rules/order.md) | Consistently order `import` statements. | 🔧 |
+| [specifier-order](docs/rules/specifier-order.md) | Consistently order named import specifiers. | 🔧 |
-See the [order](https://github.com/stormwarning/eslint-plugin-import-sorting/blob/main/docs/rules/order.md) rule docs for more configuration options.
+
diff --git a/docs/rules/order.md b/docs/rules/order.md
index 2d8d305..dd6a16a 100644
--- a/docs/rules/order.md
+++ b/docs/rules/order.md
@@ -1,8 +1,8 @@
-# import-sorting/order
+# Consistently order `import` statements (`import-sorting/order`)
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
-Enforce a convention in the order of `import` statements.
+
The grouping order is as follows:
@@ -24,13 +24,17 @@ constitutes an “internal” module.
For example:
```js
-settings: {
- // Group official React packages together.
- 'import-sorting/framework-patterns': /^react(\/|-dom|-router|$)/.source,
- // Group aliased imports together.
- 'import-sorting/internal-patterns': /^~/.source,
-},
-rules: {
- 'import-sorting/order': 'error',
-},
+export default [
+ {
+ settings: {
+ // Group official React packages together.
+ 'import-sorting/framework-patterns': /^react(\/|-dom|-router|$)/.source,
+ // Group aliased imports together.
+ 'import-sorting/internal-patterns': /^~/.source,
+ },
+ rules: {
+ 'import-sorting/order': 'error',
+ },
+ },
+]
```
diff --git a/docs/rules/specifier-order.md b/docs/rules/specifier-order.md
new file mode 100644
index 0000000..5b5c98d
--- /dev/null
+++ b/docs/rules/specifier-order.md
@@ -0,0 +1,18 @@
+# Consistently order named import specifiers (`import-sorting/specifier-order`)
+
+🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
+
+
+Specifiers are sorted naturally, the same as imports within groups. `type`
+keywords are ignored during sorting.
+
+```js
+export default [
+ {
+ rules: {
+ 'import-sorting/specifier-order': 'error',
+ },
+ },
+]
+```
diff --git a/package-lock.json b/package-lock.json
index 9b8e471..49a055a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,26 +1,26 @@
{
"name": "eslint-plugin-import-sorting",
- "version": "1.2.2",
+ "version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "eslint-plugin-import-sorting",
- "version": "1.2.2",
+ "version": "2.0.0",
"dependencies": {
- "@typescript-eslint/utils": "7.8.0",
+ "@typescript-eslint/utils": "7.18.0",
"object.groupby": "1.0.1"
},
"devDependencies": {
"@changesets/cli": "2.26.2",
"@types/eslint": "8.44.8",
"@types/node": "20.10.4",
- "@typescript-eslint/rule-tester": "7.8.0",
+ "@typescript-eslint/rule-tester": "7.18.0",
"@zazen/changesets-changelog": "2.0.3",
- "@zazen/eslint-config": "6.6.0",
+ "@zazen/eslint-config": "6.8.0",
"c8": "8.0.1",
"dedent": "1.5.3",
- "eslint": "8.56.0",
+ "eslint": "8.57.1",
"eslint-doc-generator": "1.6.1",
"eslint-plugin-eslint-plugin": "5.1.1",
"husky": "8.0.3",
@@ -838,20 +838,23 @@
}
},
"node_modules/@eslint/js": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
- "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "license": "MIT",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
- "version": "0.11.13",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
- "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
- "dependencies": {
- "@humanwhocodes/object-schema": "^2.0.1",
- "debug": "^4.1.1",
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
"minimatch": "^3.0.5"
},
"engines": {
@@ -871,9 +874,11 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
- "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw=="
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "license": "BSD-3-Clause"
},
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
@@ -1616,7 +1621,8 @@
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true
},
"node_modules/@types/json5": {
"version": "0.0.29",
@@ -1648,7 +1654,8 @@
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
- "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="
+ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
+ "dev": true
},
"node_modules/@types/yargs": {
"version": "17.0.32",
@@ -1666,16 +1673,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "6.19.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.0.tgz",
- "integrity": "sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
+ "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
- "@typescript-eslint/scope-manager": "6.19.0",
- "@typescript-eslint/type-utils": "6.19.0",
- "@typescript-eslint/utils": "6.19.0",
- "@typescript-eslint/visitor-keys": "6.19.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/type-utils": "6.21.0",
+ "@typescript-eslint/utils": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -1701,17 +1709,18 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
- "version": "6.19.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.0.tgz",
- "integrity": "sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+ "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "6.19.0",
- "@typescript-eslint/types": "6.19.0",
- "@typescript-eslint/typescript-estree": "6.19.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
"semver": "^7.5.4"
},
"engines": {
@@ -1867,15 +1876,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "6.19.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.0.tgz",
- "integrity": "sha512-1DyBLG5SH7PYCd00QlroiW60YJ4rWMuUGa/JBV0iZuqi4l4IK3twKPq5ZkEebmGqRjXWVgsUzfd3+nZveewgow==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
+ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/scope-manager": "6.19.0",
- "@typescript-eslint/types": "6.19.0",
- "@typescript-eslint/typescript-estree": "6.19.0",
- "@typescript-eslint/visitor-keys": "6.19.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1895,14 +1905,16 @@
}
},
"node_modules/@typescript-eslint/rule-tester": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-7.8.0.tgz",
- "integrity": "sha512-f1wXWeZx8XJB/z9Oyjx0ZLmhvcFelSJ0CVvOurCkrISOZhre+imIj5FQQz1rBy/Ips0dCbVl5G4MWTuzlzj5QQ==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-7.18.0.tgz",
+ "integrity": "sha512-ClrFQlwen9pJcYPIBLuarzBpONQAwjmJ0+YUjAo1TGzoZFJPyUK/A7bb4Mps0u+SMJJnFXbfMN8I9feQDf0O5A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "7.8.0",
- "@typescript-eslint/utils": "7.8.0",
+ "@typescript-eslint/typescript-estree": "7.18.0",
+ "@typescript-eslint/utils": "7.18.0",
"ajv": "^6.12.6",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
"lodash.merge": "4.6.2",
"semver": "^7.6.0"
},
@@ -1919,10 +1931,11 @@
}
},
"node_modules/@typescript-eslint/rule-tester/node_modules/@typescript-eslint/types": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz",
- "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
+ "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "^18.18.0 || >=20.0.0"
},
@@ -1932,13 +1945,14 @@
}
},
"node_modules/@typescript-eslint/rule-tester/node_modules/@typescript-eslint/typescript-estree": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz",
- "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
+ "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/types": "7.8.0",
- "@typescript-eslint/visitor-keys": "7.8.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1960,12 +1974,13 @@
}
},
"node_modules/@typescript-eslint/rule-tester/node_modules/@typescript-eslint/visitor-keys": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz",
- "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
+ "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "7.8.0",
+ "@typescript-eslint/types": "7.18.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1981,15 +1996,17 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/rule-tester/node_modules/minimatch": {
- "version": "9.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
- "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -2001,13 +2018,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "6.19.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.0.tgz",
- "integrity": "sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+ "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "6.19.0",
- "@typescript-eslint/visitor-keys": "6.19.0"
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -2018,13 +2036,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "6.19.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.0.tgz",
- "integrity": "sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
+ "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "6.19.0",
- "@typescript-eslint/utils": "6.19.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/utils": "6.21.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@@ -2045,17 +2064,18 @@
}
},
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
- "version": "6.19.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.0.tgz",
- "integrity": "sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+ "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "6.19.0",
- "@typescript-eslint/types": "6.19.0",
- "@typescript-eslint/typescript-estree": "6.19.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
"semver": "^7.5.4"
},
"engines": {
@@ -2070,10 +2090,11 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "6.19.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.0.tgz",
- "integrity": "sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
+ "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
@@ -2083,13 +2104,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "6.19.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.0.tgz",
- "integrity": "sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+ "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/types": "6.19.0",
- "@typescript-eslint/visitor-keys": "6.19.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -2115,6 +2137,7 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -2124,6 +2147,7 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -2135,17 +2159,15 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz",
- "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
+ "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
+ "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
- "@types/json-schema": "^7.0.15",
- "@types/semver": "^7.5.8",
- "@typescript-eslint/scope-manager": "7.8.0",
- "@typescript-eslint/types": "7.8.0",
- "@typescript-eslint/typescript-estree": "7.8.0",
- "semver": "^7.6.0"
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -2159,12 +2181,13 @@
}
},
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz",
- "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
+ "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "7.8.0",
- "@typescript-eslint/visitor-keys": "7.8.0"
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -2175,9 +2198,10 @@
}
},
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz",
- "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
+ "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
+ "license": "MIT",
"engines": {
"node": "^18.18.0 || >=20.0.0"
},
@@ -2187,12 +2211,13 @@
}
},
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz",
- "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
+ "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
+ "license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/types": "7.8.0",
- "@typescript-eslint/visitor-keys": "7.8.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -2214,11 +2239,12 @@
}
},
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz",
- "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==",
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
+ "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "7.8.0",
+ "@typescript-eslint/types": "7.18.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -2233,14 +2259,16 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/utils/node_modules/minimatch": {
- "version": "9.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
- "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -2252,12 +2280,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "6.19.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.0.tgz",
- "integrity": "sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+ "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "6.19.0",
+ "@typescript-eslint/types": "6.21.0",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -2506,20 +2535,21 @@
}
},
"node_modules/@zazen/eslint-config": {
- "version": "6.6.0",
- "resolved": "https://registry.npmjs.org/@zazen/eslint-config/-/eslint-config-6.6.0.tgz",
- "integrity": "sha512-T7s+1DSRzK2nUc3lV3Heht2ATHiz+M0s8O9hejqOOrF7Y2PoopXZSiif2OhtAfTPqdGm7URk0Mal0QZ6VvKr9w==",
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/@zazen/eslint-config/-/eslint-config-6.8.0.tgz",
+ "integrity": "sha512-bJ398ZElSYayF24tYq1wQTt4598wC+kxMBVoBY6aQFxta0GpI/Ml7Eza+XbP0GjPhC1hyVWpyl9gFJ0aw/OuKw==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"@rushstack/eslint-patch": "1.7.0",
- "@typescript-eslint/eslint-plugin": "6.19.0",
- "@typescript-eslint/parser": "6.19.0",
+ "@typescript-eslint/eslint-plugin": "6.21.0",
+ "@typescript-eslint/parser": "6.21.0",
"eslint-config-prettier": "9.1.0",
"eslint-config-xo": "0.43.1",
"eslint-config-xo-typescript": "1.0.1",
"eslint-plugin-etc": "2.0.3",
"eslint-plugin-import": "2.29.1",
- "eslint-plugin-import-sorting": "1.0.0",
+ "eslint-plugin-import-sorting": "1.1.0",
"eslint-plugin-n": "16.6.2",
"eslint-plugin-prefer-let": "3.0.1",
"eslint-plugin-promise": "6.1.1",
@@ -2548,15 +2578,6 @@
"node": ">=8"
}
},
- "node_modules/@zazen/eslint-config/node_modules/eslint-plugin-import-sorting": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import-sorting/-/eslint-plugin-import-sorting-1.0.0.tgz",
- "integrity": "sha512-2X1GqpEBMhx23QA4A580N5UJ9LOxTKxXJr68V7n39qytKCOarBOqasDBRSOfLbsXaOzXVcbTzdr6CCt3sUxf6w==",
- "dev": true,
- "dependencies": {
- "object.groupby": "1.0.1"
- }
- },
"node_modules/@zazen/eslint-config/node_modules/eslint-plugin-unicorn": {
"version": "50.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz",
@@ -4062,15 +4083,17 @@
}
},
"node_modules/eslint": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
- "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
- "@eslint/js": "8.56.0",
- "@humanwhocodes/config-array": "^0.11.13",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
@@ -4494,6 +4517,18 @@
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
}
},
+ "node_modules/eslint-plugin-import-sorting": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import-sorting/-/eslint-plugin-import-sorting-1.1.0.tgz",
+ "integrity": "sha512-4qGrkAU8/YRXMaegYHLzZpy9rcFK0UX4nQglHGkecEBzsfyWNBIfoS1R67jJylL1buQrH8c2iHkZXgWhNsi+Rw==",
+ "dev": true,
+ "dependencies": {
+ "object.groupby": "1.0.1"
+ },
+ "engines": {
+ "node": ">=16.17.0 || >=18.6.0"
+ }
+ },
"node_modules/eslint-plugin-import/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
diff --git a/package.json b/package.json
index f55453b..c5b9e19 100644
--- a/package.json
+++ b/package.json
@@ -67,19 +67,19 @@
]
},
"dependencies": {
- "@typescript-eslint/utils": "7.8.0",
+ "@typescript-eslint/utils": "7.18.0",
"object.groupby": "1.0.1"
},
"devDependencies": {
"@changesets/cli": "2.26.2",
"@types/eslint": "8.44.8",
"@types/node": "20.10.4",
- "@typescript-eslint/rule-tester": "7.8.0",
+ "@typescript-eslint/rule-tester": "7.18.0",
"@zazen/changesets-changelog": "2.0.3",
- "@zazen/eslint-config": "6.6.0",
+ "@zazen/eslint-config": "6.8.0",
"c8": "8.0.1",
"dedent": "1.5.3",
- "eslint": "8.56.0",
+ "eslint": "8.57.1",
"eslint-doc-generator": "1.6.1",
"eslint-plugin-eslint-plugin": "5.1.1",
"husky": "8.0.3",
diff --git a/src/index.ts b/src/index.ts
index 318a5c0..e3371b4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,9 +1,11 @@
import order from './rules/order.js'
+import specifierOrder from './rules/specifier-order.js'
const plugin = {
- name: 'sorting-order',
+ name: 'import-sorting',
rules: {
order,
+ 'specifier-order': specifierOrder,
},
}
diff --git a/src/rules/order.ts b/src/rules/order.ts
index 8894445..305deb9 100644
--- a/src/rules/order.ts
+++ b/src/rules/order.ts
@@ -1,13 +1,15 @@
-import { AST_NODE_TYPES, ESLintUtils, type TSESTree, type TSESLint } from '@typescript-eslint/utils'
+import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/utils'
-import { compare } from '../utils/compare.js'
-import { computeGroup, isSideEffectImport } from '../utils/compute-group.js'
-import { getCommentBefore } from '../utils/get-comment.js'
+import { computeGroup } from '../utils/compute-group.js'
+import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines.js'
import { getGroupNumber } from '../utils/get-group-number.js'
-import { getLinesBetween } from '../utils/get-lines-between.js'
-import { getNodeRange } from '../utils/get-node-range.js'
+import { getNewlineErrors } from '../utils/get-newline-errors.js'
+import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled.js'
+import { makeFixes } from '../utils/make-fixes.js'
+import { makeNewlineFixes } from '../utils/make-newline-fixes.js'
import { pairwise } from '../utils/pairwise.js'
-import { sortNodes } from '../utils/sort-nodes.js'
+import { rangeToDiff } from '../utils/range-to-diff.js'
+import { sortNodesByGroups } from '../utils/sort-nodes-by-groups.js'
import type { ImportDeclarationNode, Options, SortingNode } from '../utils/types.js'
export const IMPORT_GROUPS = [
@@ -22,19 +24,21 @@ export const IMPORT_GROUPS = [
'unknown',
] as const
+type MessageId = 'out-of-order' | 'needs-newline' | 'extra-newline'
+
// eslint-disable-next-line new-cap
const createRule = ESLintUtils.RuleCreator(
(name) =>
`https://github.com/stormwarning/eslint-plugin-import-sorting/blob/main/docs/rules/${name}.md`,
)
-export default createRule({
+export default createRule({
name: 'order',
meta: {
type: 'suggestion',
fixable: 'code',
docs: {
- description: 'Enforce a convention in the order of `import` statements.',
+ description: 'Consistently order `import` statements.',
},
messages: {
'needs-newline':
@@ -53,6 +57,10 @@ export default createRule({
order: 'asc',
type: 'natural',
}
+ let eslintDisabledLines = getEslintDisabledLines({
+ ruleName: context.id,
+ sourceCode,
+ })
let nodes: SortingNode[] = []
function registerNode(node: ImportDeclarationNode) {
@@ -63,19 +71,20 @@ export default createRule({
} else if (node.type === AST_NODE_TYPES.TSImportEqualsDeclaration) {
name =
node.moduleReference.type === AST_NODE_TYPES.TSExternalModuleReference
- ? // @ts-expect-error -- `value` is not in the type definition.
- `${node.moduleReference.expression.value}`
- : sourceCode.text.slice(...node.moduleReference.range)
+ ? node.moduleReference.expression.value
+ : sourceCode.getText(node.moduleReference)
} else {
let decl = node.declarations[0].init as TSESTree.CallExpression
- let declValue = (decl.arguments[0] as TSESTree.Literal).value
- name = declValue!.toString()
+ let { value } = decl.arguments[0] as TSESTree.Literal
+ name = value!.toString()
}
nodes.push({
group: computeGroup(node, settings, sourceCode),
- node,
+ isEslintDisabled: isNodeEslintDisabled(node, eslintDisabledLines),
name,
+ node,
+ size: rangeToDiff(node, sourceCode),
})
}
@@ -95,190 +104,91 @@ export default createRule({
},
// eslint-disable-next-line @typescript-eslint/naming-convention
'Program:exit'() {
- let hasContentBetweenNodes = (left: SortingNode, right: SortingNode): boolean =>
- sourceCode.getTokensBetween(
- left.node,
- getCommentBefore(right.node, sourceCode) ?? right.node,
- {
- includeComments: true,
- },
- ).length > 0
+ function hasContentBetweenNodes(left: SortingNode, right: SortingNode): boolean {
+ return (
+ sourceCode.getTokensBetween(left.node, right.node, {
+ includeComments: false,
+ }).length > 0
+ )
+ }
- let splittedNodes: SortingNode[][] = [[]]
+ let formattedNodes: SortingNode[][] = [[]]
for (let node of nodes) {
- let lastNode = splittedNodes.at(-1)?.at(-1)
+ let lastNode = formattedNodes.at(-1)?.at(-1)
if (lastNode && hasContentBetweenNodes(lastNode, node)) {
- splittedNodes.push([node])
+ /**
+ * Including `node` in this empty array allows groups
+ * of imports separated by other statements to be
+ * sorted, but may break other aspects.
+ */
+ formattedNodes.push([node])
} else {
- splittedNodes.at(-1)!.push(node)
+ formattedNodes.at(-1)!.push(node)
}
}
- for (let nodeList of splittedNodes) {
+ for (let nodeList of formattedNodes) {
+ let sortedNodes = sortNodesByGroups(nodeList, options, {})
pairwise(nodeList, (left, right) => {
let leftNumber = getGroupNumber(IMPORT_GROUPS, left)
let rightNumber = getGroupNumber(IMPORT_GROUPS, right)
+ let indexOfLeft = sortedNodes.indexOf(left)
+ let indexOfRight = sortedNodes.indexOf(right)
- let numberOfEmptyLinesBetween = getLinesBetween(sourceCode, left, right)
+ let messages: MessageId[] = []
- if (
- !(
- isSideEffectImport(left.node, sourceCode) &&
- isSideEffectImport(right.node, sourceCode)
- ) &&
- !hasContentBetweenNodes(left, right) &&
- (leftNumber > rightNumber ||
- (leftNumber === rightNumber && compare(left, right, options) > 0))
- ) {
- context.report({
- messageId: 'out-of-order',
- data: {
- left: left.name,
- right: right.name,
- },
- node: right.node,
- fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
- })
+ if (indexOfLeft > indexOfRight) {
+ messages.push(
+ leftNumber === rightNumber ? 'out-of-order' : 'out-of-order',
+ )
}
- if (options.newlinesBetween === 'never' && numberOfEmptyLinesBetween > 0) {
+ messages = [
+ ...messages,
+ ...getNewlineErrors({
+ missingLineError: 'needs-newline',
+ extraLineError: 'extra-newline',
+ left,
+ leftNumber,
+ right,
+ rightNumber,
+ sourceCode,
+ options,
+ }),
+ ]
+
+ for (let message of messages) {
context.report({
- messageId: 'extra-newline',
+ fix: (fixer) => [
+ ...makeFixes({
+ fixer,
+ nodes: nodeList,
+ sortedNodes,
+ sourceCode,
+ }),
+ ...makeNewlineFixes({
+ fixer,
+ nodes: nodeList,
+ sortedNodes,
+ sourceCode,
+ options,
+ }),
+ ],
data: {
- left: left.name,
+ rightGroup: right.group,
+ leftGroup: left.group,
right: right.name,
+ left: left.name,
},
node: right.node,
- fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
+ messageId: message,
})
}
-
- if (options.newlinesBetween === 'always') {
- if (leftNumber < rightNumber && numberOfEmptyLinesBetween === 0) {
- context.report({
- messageId: 'needs-newline',
- data: {
- left: left.name,
- right: right.name,
- },
- node: right.node,
- fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
- })
- } else if (
- numberOfEmptyLinesBetween > 1 ||
- (leftNumber === rightNumber && numberOfEmptyLinesBetween > 0)
- ) {
- context.report({
- messageId: 'extra-newline',
- data: {
- left: left.name,
- right: right.name,
- },
- node: right.node,
- fix: (fixer) => fix(fixer, nodeList, sourceCode, options),
- })
- }
- }
})
}
},
}
},
})
-
-function fix(
- fixer: TSESLint.RuleFixer,
- nodesToFix: SortingNode[],
- sourceCode: TSESLint.SourceCode,
- options: Options,
-): TSESLint.RuleFix[] {
- let fixes: TSESLint.RuleFix[] = []
- let grouped: Record = {}
-
- for (let node of nodesToFix) {
- let groupNumber = getGroupNumber(IMPORT_GROUPS, node)
-
- grouped[groupNumber] =
- groupNumber in grouped ? sortNodes([...grouped[groupNumber], node], options) : [node]
- }
-
- let formatted = Object.keys(grouped)
- .sort((a, b) => Number(a) - Number(b))
- .reduce(
- (accumulator: SortingNode[], group: string) => [...accumulator, ...grouped[group]],
- [],
- )
-
- for (let max = formatted.length, index = 0; index < max; index++) {
- let node = formatted.at(index)!
-
- fixes.push(
- fixer.replaceTextRange(
- getNodeRange(nodesToFix.at(index)!.node, sourceCode),
- sourceCode.text.slice(...getNodeRange(node.node, sourceCode)),
- ),
- )
-
- if (options.newlinesBetween !== 'ignore') {
- let nextNode = formatted.at(index + 1)
-
- if (nextNode) {
- let linesBetweenImports = getLinesBetween(
- sourceCode,
- nodesToFix.at(index)!,
- nodesToFix.at(index + 1)!,
- )
-
- if (
- (options.newlinesBetween === 'always' &&
- getGroupNumber(IMPORT_GROUPS, node) ===
- getGroupNumber(IMPORT_GROUPS, nextNode) &&
- linesBetweenImports !== 0) ||
- (options.newlinesBetween === 'never' && linesBetweenImports > 0)
- ) {
- fixes.push(
- fixer.removeRange([
- getNodeRange(nodesToFix.at(index)!.node, sourceCode).at(1)!,
- getNodeRange(nodesToFix.at(index + 1)!.node, sourceCode).at(0)! - 1,
- ]),
- )
- }
-
- if (
- options.newlinesBetween === 'always' &&
- getGroupNumber(IMPORT_GROUPS, node) !==
- getGroupNumber(IMPORT_GROUPS, nextNode) &&
- linesBetweenImports > 1
- ) {
- fixes.push(
- fixer.replaceTextRange(
- [
- getNodeRange(nodesToFix.at(index)!.node, sourceCode).at(1)!,
- getNodeRange(nodesToFix.at(index + 1)!.node, sourceCode).at(0)! - 1,
- ],
- '\n',
- ),
- )
- }
-
- if (
- options.newlinesBetween === 'always' &&
- getGroupNumber(IMPORT_GROUPS, node) !==
- getGroupNumber(IMPORT_GROUPS, nextNode) &&
- linesBetweenImports === 0
- ) {
- fixes.push(
- fixer.insertTextAfterRange(
- getNodeRange(nodesToFix.at(index)!.node, sourceCode),
- '\n',
- ),
- )
- }
- }
- }
- }
-
- return fixes
-}
diff --git a/src/rules/specifier-order.ts b/src/rules/specifier-order.ts
new file mode 100644
index 0000000..f1577bd
--- /dev/null
+++ b/src/rules/specifier-order.ts
@@ -0,0 +1,155 @@
+import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'
+
+import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines.js'
+import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled.js'
+import { isSortable } from '../utils/is-sortable.js'
+import { makeFixes } from '../utils/make-fixes.js'
+import { pairwise } from '../utils/pairwise.js'
+import { rangeToDiff } from '../utils/range-to-diff.js'
+import { sortNodes } from '../utils/sort-nodes.js'
+import type { MemberSortingNode } from '../utils/types.js'
+
+type ValuesFirstOrder = ['value', 'type']
+type TypesFirstOrder = ['type', 'value']
+type MixedOrder = ['any']
+
+interface Options {
+ ignoreAlias?: boolean
+ ignoreCase?: boolean
+ groupKind?: 'values-first' | 'types-first' | 'mixed'
+ order: 'asc' | 'desc'
+ type: 'natural'
+}
+
+type MessageId = 'specifier-out-of-order'
+
+// eslint-disable-next-line new-cap
+const createRule = ESLintUtils.RuleCreator(
+ (name) =>
+ `https://github.com/stormwarning/eslint-plugin-import-sorting/blob/main/docs/rules/${name}.md`,
+)
+
+export default createRule({
+ name: 'specifier-order',
+ meta: {
+ type: 'suggestion',
+ fixable: 'code',
+ docs: {
+ description: 'Consistently order named import specifiers.',
+ },
+ messages: {
+ 'specifier-out-of-order': '{{right}} should occur before {{left}}',
+ },
+ schema: [],
+ },
+ defaultOptions: [],
+ create(context) {
+ let { sourceCode } = context
+ let options: Options = {
+ ignoreAlias: true,
+ ignoreCase: true,
+ groupKind: 'mixed',
+ order: 'asc',
+ type: 'natural',
+ }
+
+ return {
+ ImportDeclaration(node) {
+ let specifiers = node.specifiers.filter(
+ ({ type }) => type === AST_NODE_TYPES.ImportSpecifier,
+ )
+ if (!isSortable(specifiers)) return
+
+ let eslintDisabledLines = getEslintDisabledLines({
+ ruleName: context.id,
+ sourceCode,
+ })
+
+ let formattedMembers: MemberSortingNode[][] = [[]]
+ for (let specifier of specifiers) {
+ let { name } = specifier.local
+
+ if (specifier.type === AST_NODE_TYPES.ImportSpecifier && options.ignoreAlias) {
+ name =
+ specifier.imported.type === AST_NODE_TYPES.Identifier
+ ? specifier.imported.name
+ : // @ts-expect-error -- `value` possibly missing from type declarations.
+ (specifier.imported.value as string)
+ }
+
+ let sortingNode: MemberSortingNode = {
+ groupKind:
+ specifier.type === AST_NODE_TYPES.ImportSpecifier &&
+ specifier.importKind === 'type'
+ ? 'type'
+ : 'value',
+ isEslintDisabled: isNodeEslintDisabled(specifier, eslintDisabledLines),
+ size: rangeToDiff(specifier, sourceCode),
+ node: specifier,
+ name,
+ }
+
+ formattedMembers.at(-1)!.push(sortingNode)
+ }
+
+ let groupKindOrder: ValuesFirstOrder | TypesFirstOrder | MixedOrder
+ if (options.groupKind === 'values-first') {
+ groupKindOrder = ['value', 'type']
+ } else if (options.groupKind === 'types-first') {
+ groupKindOrder = ['type', 'value']
+ } else {
+ groupKindOrder = ['any']
+ }
+
+ for (let nodes of formattedMembers) {
+ let filteredGroupKindNodes = groupKindOrder.map(
+ (groupKind: 'value' | 'type' | 'any') =>
+ nodes.filter(
+ (currentNode) =>
+ groupKind === 'any' || currentNode.groupKind === groupKind,
+ ),
+ )
+ let sortNodesExcludingEslintDisabled = (
+ ignoreEslintDisabledNodes: boolean,
+ ): MemberSortingNode[] =>
+ filteredGroupKindNodes.flatMap((groupedNodes) =>
+ sortNodes(groupedNodes, options, {
+ ignoreEslintDisabledNodes,
+ }),
+ )
+ let sortedNodes = sortNodesExcludingEslintDisabled(false)
+ let sortedNodesExcludingEslintDisabled = sortNodesExcludingEslintDisabled(true)
+
+ pairwise(nodes, (left, right) => {
+ let indexOfLeft = sortedNodes.indexOf(left)
+ let indexOfRight = sortedNodes.indexOf(right)
+ let indexOfRightExcludingEslintDisabled =
+ sortedNodesExcludingEslintDisabled.indexOf(right)
+ if (
+ indexOfLeft < indexOfRight &&
+ indexOfLeft < indexOfRightExcludingEslintDisabled
+ ) {
+ return
+ }
+
+ context.report({
+ fix: (fixer) =>
+ makeFixes({
+ fixer,
+ nodes,
+ sortedNodes: sortedNodesExcludingEslintDisabled,
+ sourceCode,
+ }),
+ data: {
+ right: right.name,
+ left: left.name,
+ },
+ messageId: 'specifier-out-of-order',
+ node: right.node,
+ })
+ })
+ }
+ },
+ }
+ },
+})
diff --git a/src/utils/compare.ts b/src/utils/compare.ts
index 4212103..b93a6ba 100644
--- a/src/utils/compare.ts
+++ b/src/utils/compare.ts
@@ -1,32 +1,32 @@
import type { SortingNode } from './types.js'
-interface BaseCompareOptions {
+interface BaseCompareOptions {
/**
* Custom function to get the value of the node. By default, returns the
* node's name.
*/
- nodeValueGetter?: (node: SortingNode) => string
+ nodeValueGetter?: (node: T) => string
order: 'desc' | 'asc'
}
-interface NaturalCompareOptions extends BaseCompareOptions {
+interface NaturalCompareOptions extends BaseCompareOptions {
ignoreCase?: boolean
type: 'natural'
}
-export type CompareOptions = NaturalCompareOptions
+export type CompareOptions = NaturalCompareOptions
-export function compare(a: SortingNode, b: SortingNode, options: CompareOptions): number {
- /** Don't sort unsassigned imports. */
+export function compare(a: T, b: T, options: CompareOptions): number {
+ /** Don't sort unassigned imports. */
if (a.group === 'unassigned' || b.group === 'unassigned') return 0
if (b.dependencies?.includes(a.name)) return -1
if (a.dependencies?.includes(b.name)) return 1
let orderCoefficient = options.order === 'asc' ? 1 : -1
- let sortingFunction: (a: SortingNode, b: SortingNode) => number
+ let sortingFunction: (a: T, b: T) => number
- let nodeValueGetter = options.nodeValueGetter ?? ((node: SortingNode) => node.name)
+ let nodeValueGetter = options.nodeValueGetter ?? ((node: T) => node.name)
sortingFunction = (aNode, bNode) => {
let aImport = stripProtocol(nodeValueGetter(aNode))
diff --git a/src/utils/get-comment.ts b/src/utils/get-comment.ts
index bd2c898..5abce58 100644
--- a/src/utils/get-comment.ts
+++ b/src/utils/get-comment.ts
@@ -1,35 +1,49 @@
import { AST_TOKEN_TYPES, type TSESLint, type TSESTree } from '@typescript-eslint/utils'
-export function getCommentBefore(
+/**
+ * Returns a list of comments before a given node, excluding ones that are
+ * right after code. Includes comment blocks.
+ */
+export function getCommentsBefore(
node: TSESTree.Node,
source: TSESLint.SourceCode,
-): TSESTree.Comment | undefined {
- let [tokenBefore, tokenOrCommentBefore] = source.getTokensBefore(node, {
- filter: ({ value, type }) =>
- !(type === AST_TOKEN_TYPES.Punctuator && [',', ';'].includes(value)),
- includeComments: true,
- count: 2,
- }) as Array
+ tokenValueToIgnoreBefore?: string,
+): TSESTree.Comment[] {
+ let commentsBefore = getCommentsBeforeNodeOrToken(node, source)
+ let tokenBeforeNode = source.getTokenBefore(node)
if (
- (tokenOrCommentBefore?.type === AST_TOKEN_TYPES.Block ||
- tokenOrCommentBefore?.type === AST_TOKEN_TYPES.Line) &&
- node.loc.start.line - tokenOrCommentBefore.loc.end.line <= 1 &&
- tokenBefore?.loc.end.line !== tokenOrCommentBefore.loc.start.line
+ commentsBefore.length > 0 ||
+ !tokenValueToIgnoreBefore ||
+ tokenBeforeNode?.value !== tokenValueToIgnoreBefore
) {
- return tokenOrCommentBefore
+ return commentsBefore
}
- return undefined
+ return getCommentsBeforeNodeOrToken(tokenBeforeNode, source)
+}
+
+function getCommentsBeforeNodeOrToken(
+ node: TSESTree.Node | TSESTree.Token,
+ source: TSESLint.SourceCode,
+): TSESTree.Comment[] {
+ /**
+ * `getCommentsBefore` also returns comments that are right after code,
+ * filter those out.
+ */
+ return source.getCommentsBefore(node).filter((comment) => {
+ let tokenBeforeComment = source.getTokenBefore(comment)
+ return tokenBeforeComment?.loc.end.line !== comment.loc.end.line
+ })
}
export function getCommentAfter(
- node: TSESTree.Node,
+ node: TSESTree.Node | TSESTree.Token,
source: TSESLint.SourceCode,
): TSESTree.Comment | undefined {
let token = source.getTokenAfter(node, {
filter: ({ value, type }) =>
- !(type === AST_TOKEN_TYPES.Punctuator && [',', ';'].includes(value)),
+ !(type === AST_TOKEN_TYPES.Punctuator && [',', ';', ':'].includes(value)),
includeComments: true,
})
diff --git a/src/utils/get-eslint-disabled-lines.ts b/src/utils/get-eslint-disabled-lines.ts
new file mode 100644
index 0000000..da886cd
--- /dev/null
+++ b/src/utils/get-eslint-disabled-lines.ts
@@ -0,0 +1,50 @@
+import type { TSESLint } from '@typescript-eslint/utils'
+
+import { getEslintDisabledRules } from './get-eslint-disabled-rules.js'
+
+export function getEslintDisabledLines({
+ ruleName,
+ sourceCode,
+}: {
+ ruleName: string
+ sourceCode: TSESLint.SourceCode
+}): number[] {
+ let returnValue: number[] = []
+ // eslint-disable-next-line no-undef-init
+ let lineRulePermanentlyDisabled: number | undefined = undefined
+
+ for (let comment of sourceCode.getAllComments()) {
+ let eslintDisabledRules = getEslintDisabledRules(comment.value)
+ let isRuleDisabled =
+ eslintDisabledRules?.rules === 'all' || eslintDisabledRules?.rules.includes(ruleName)
+
+ if (!isRuleDisabled) continue
+
+ switch (eslintDisabledRules?.eslintDisableDirective) {
+ case 'eslint-disable-next-line':
+ returnValue.push(comment.loc.end.line + 1)
+ continue
+ case 'eslint-disable-line':
+ returnValue.push(comment.loc.start.line)
+ continue
+ case 'eslint-disable':
+ lineRulePermanentlyDisabled ??= comment.loc.start.line
+ break
+ case 'eslint-enable':
+ if (!lineRulePermanentlyDisabled) continue
+
+ returnValue.push(
+ ...createArrayFromTo(lineRulePermanentlyDisabled + 1, comment.loc.start.line),
+ )
+ lineRulePermanentlyDisabled = undefined
+ break
+ default:
+ break
+ }
+ }
+
+ return returnValue
+}
+
+let createArrayFromTo = (index_: number, index: number): number[] =>
+ Array.from({ length: index - index_ + 1 }, (_, item) => index_ + item)
diff --git a/src/utils/get-eslint-disabled-rules.ts b/src/utils/get-eslint-disabled-rules.ts
new file mode 100644
index 0000000..38c602a
--- /dev/null
+++ b/src/utils/get-eslint-disabled-rules.ts
@@ -0,0 +1,46 @@
+const ESLINT_DIRECTIVES = [
+ 'eslint-disable',
+ 'eslint-enable',
+ 'eslint-disable-line',
+ 'eslint-disable-next-line',
+] as const
+
+export type EslintDisableDirective = (typeof ESLINT_DIRECTIVES)[number]
+
+export function getEslintDisabledRules(comment: string):
+ | {
+ eslintDisableDirective: EslintDisableDirective
+ rules: string[] | 'all'
+ }
+ | undefined {
+ for (let eslintDisableDirective of ESLINT_DIRECTIVES) {
+ let disabledRules = getEslintDisabledRulesByType(comment, eslintDisableDirective)
+ if (disabledRules) {
+ return {
+ eslintDisableDirective,
+ rules: disabledRules,
+ }
+ }
+ }
+
+ return undefined
+}
+
+function getEslintDisabledRulesByType(
+ comment: string,
+ eslintDisableDirective: EslintDisableDirective,
+): string[] | 'all' | undefined {
+ let trimmedCommentValue = comment.trim()
+
+ if (eslintDisableDirective === trimmedCommentValue) return 'all' as const
+
+ let regexp = new RegExp(`^${eslintDisableDirective} ((?:.|\\s)*)$`)
+ let disabledRulesMatch = trimmedCommentValue.match(regexp)
+
+ if (!disabledRulesMatch) return undefined
+
+ return disabledRulesMatch[1]
+ .split(',')
+ .map((rule) => rule.trim())
+ .filter(Boolean)
+}
diff --git a/src/utils/get-newline-errors.ts b/src/utils/get-newline-errors.ts
new file mode 100644
index 0000000..390f799
--- /dev/null
+++ b/src/utils/get-newline-errors.ts
@@ -0,0 +1,50 @@
+import type { TSESLint } from '@typescript-eslint/utils'
+
+import { getLinesBetween } from './get-lines-between.js'
+import type { SortingNode } from './types.js'
+
+interface Options {
+ newlinesBetween: 'ignore' | 'always' | 'never'
+}
+
+interface GetNewlineErrorsParameters {
+ missingLineError: T
+ extraLineError: T
+ right: SortingNode
+ rightNumber: number
+ left: SortingNode
+ leftNumber: number
+ sourceCode: TSESLint.SourceCode
+ options: Options
+}
+
+export function getNewlineErrors({
+ missingLineError,
+ extraLineError,
+ right,
+ rightNumber,
+ left,
+ leftNumber,
+ sourceCode,
+ options,
+}: GetNewlineErrorsParameters): T[] {
+ let errors: T[] = []
+ let numberOfEmptyLinesBetween = getLinesBetween(sourceCode, left, right)
+
+ if (options.newlinesBetween === 'never' && numberOfEmptyLinesBetween > 0) {
+ errors.push(extraLineError)
+ }
+
+ if (options.newlinesBetween === 'always') {
+ if (leftNumber < rightNumber && numberOfEmptyLinesBetween === 0) {
+ errors.push(missingLineError)
+ } else if (
+ numberOfEmptyLinesBetween > 1 ||
+ (leftNumber === rightNumber && numberOfEmptyLinesBetween > 0)
+ ) {
+ errors.push(extraLineError)
+ }
+ }
+
+ return errors
+}
diff --git a/src/utils/get-node-range.ts b/src/utils/get-node-range.ts
index 31adad2..2b64da2 100644
--- a/src/utils/get-node-range.ts
+++ b/src/utils/get-node-range.ts
@@ -1,11 +1,13 @@
-import { ASTUtils, type TSESLint, type TSESTree } from '@typescript-eslint/utils'
+import { ASTUtils, type TSESLint, type TSESTree, AST_TOKEN_TYPES } from '@typescript-eslint/utils'
-import { getCommentBefore } from './get-comment.js'
+import { getCommentsBefore } from './get-comment.js'
+import { getEslintDisabledRules } from './get-eslint-disabled-rules.js'
export function getNodeRange(
node: TSESTree.Node,
sourceCode: TSESLint.SourceCode,
additionalOptions?: {
+ ignoreHighestBlockComment?: boolean
partitionComment?: string[] | boolean | string
},
): TSESTree.Range {
@@ -23,21 +25,43 @@ export function getNodeRange(
end = bodyClosingParen.range.at(1)!
}
- let comment = getCommentBefore(node, sourceCode)
+ let comments = getCommentsBefore(node, sourceCode)
+ let highestBlockComment = comments.find((comment) => comment.type === AST_TOKEN_TYPES.Block)
+ let relevantTopComment: TSESTree.Comment | undefined
- if (raw.endsWith(';') || raw.endsWith(',')) {
- let tokensAfter = sourceCode.getTokensAfter(node, {
- includeComments: true,
- count: 2,
- })
+ /**
+ * Iterate on all comments starting from the bottom until we reach the last
+ * of the comments, a newline between comments, a partition comment,
+ * or an eslint-disable comment.
+ */
+ for (let index = comments.length - 1; index >= 0; index--) {
+ let comment = comments[index]
- if (node.loc.start.line === tokensAfter.at(1)?.loc.start.line) {
- end -= 1
+ let eslintDisabledRules = getEslintDisabledRules(comment.value)
+ if (
+ eslintDisabledRules?.eslintDisableDirective === 'eslint-disable' ||
+ eslintDisabledRules?.eslintDisableDirective === 'eslint-enable'
+ ) {
+ break
}
+
+ // Check for newlines between comments or between the first comment and
+ // the node.
+ let previousCommentOrNodeStartLine =
+ index === comments.length - 1 ? node.loc.start.line : comments[index + 1].loc.start.line
+ if (comment.loc.end.line !== previousCommentOrNodeStartLine - 1) {
+ break
+ }
+
+ if (additionalOptions?.ignoreHighestBlockComment && comment === highestBlockComment) {
+ break
+ }
+
+ relevantTopComment = comment
}
- if (comment) {
- start = comment.range.at(0)!
+ if (relevantTopComment) {
+ start = relevantTopComment.range.at(0)!
}
return [start, end]
diff --git a/src/utils/is-node-eslint-disabled.ts b/src/utils/is-node-eslint-disabled.ts
new file mode 100644
index 0000000..b23c04c
--- /dev/null
+++ b/src/utils/is-node-eslint-disabled.ts
@@ -0,0 +1,5 @@
+import type { TSESTree } from '@typescript-eslint/utils'
+
+export function isNodeEslintDisabled(node: TSESTree.Node, eslintDisabledLines: number[]): boolean {
+ return eslintDisabledLines.includes(node.loc.start.line)
+}
diff --git a/src/utils/is-sortable.ts b/src/utils/is-sortable.ts
new file mode 100644
index 0000000..5dd977b
--- /dev/null
+++ b/src/utils/is-sortable.ts
@@ -0,0 +1,3 @@
+export function isSortable(node: unknown): boolean {
+ return Array.isArray(node) && node.length > 1
+}
diff --git a/src/utils/make-comment-after-fixes.ts b/src/utils/make-comment-after-fixes.ts
new file mode 100644
index 0000000..3119bff
--- /dev/null
+++ b/src/utils/make-comment-after-fixes.ts
@@ -0,0 +1,40 @@
+import type { TSESLint, TSESTree } from '@typescript-eslint/utils'
+
+import { getCommentAfter } from './get-comment.js'
+
+interface CommentAfterFixesParameters {
+ fixer: TSESLint.RuleFixer
+ node: TSESTree.Token | TSESTree.Node
+ sortedNode: TSESTree.Token | TSESTree.Node
+ sourceCode: TSESLint.SourceCode
+}
+
+export function makeCommentAfterFixes({
+ fixer,
+ node,
+ sortedNode,
+ sourceCode,
+}: CommentAfterFixesParameters): TSESLint.RuleFix[] {
+ let commentAfter = getCommentAfter(sortedNode, sourceCode)
+ let isNodesOnSameLine = node.loc.start.line === sortedNode.loc.end.line
+
+ if (!commentAfter || isNodesOnSameLine) return []
+
+ let fixes: TSESLint.RuleFix[] = []
+ let tokenBefore = sourceCode.getTokenBefore(commentAfter)
+
+ let range: TSESTree.Range = [tokenBefore!.range.at(1)!, commentAfter.range.at(1)!]
+
+ fixes.push(fixer.replaceTextRange(range, ''))
+
+ let tokenAfterNode = sourceCode.getTokenAfter(node)
+
+ fixes.push(
+ fixer.insertTextAfter(
+ tokenAfterNode?.loc.end.line === node.loc.end.line ? tokenAfterNode : node,
+ sourceCode.text.slice(...range),
+ ),
+ )
+
+ return fixes
+}
diff --git a/src/utils/make-fixes.ts b/src/utils/make-fixes.ts
new file mode 100644
index 0000000..16fd6be
--- /dev/null
+++ b/src/utils/make-fixes.ts
@@ -0,0 +1,82 @@
+import type { TSESLint } from '@typescript-eslint/utils'
+
+import { getNodeRange } from './get-node-range.js'
+import { makeCommentAfterFixes } from './make-comment-after-fixes.js'
+import type { SortingNode } from './types.js'
+
+interface MakeFixesParameters {
+ fixer: TSESLint.RuleFixer
+ nodes: SortingNode[]
+ sortedNodes: SortingNode[]
+ sourceCode: TSESLint.SourceCode
+ ignoreFirstNodeHighestBlockComment?: boolean
+}
+
+export function makeFixes({
+ fixer,
+ nodes,
+ sortedNodes,
+ sourceCode,
+ ignoreFirstNodeHighestBlockComment,
+}: MakeFixesParameters): TSESLint.RuleFix[] {
+ let fixes: TSESLint.RuleFix[] = []
+
+ for (let max = nodes.length, index = 0; index < max; index++) {
+ let sortingNode = nodes.at(index)!
+ let sortedSortingNode = sortedNodes.at(index)!
+ let { node } = sortingNode
+ let { node: sortedNode } = sortedSortingNode
+ let isNodeFirstNode = node === nodes.at(0)!.node
+ let isSortedNodeFirstNode = sortedNode === nodes.at(0)!.node
+
+ if (node === sortedNode) {
+ continue
+ }
+
+ let sortedNodeCode = sourceCode.text.slice(
+ ...getNodeRange(sortedNode, sourceCode, {
+ ignoreHighestBlockComment:
+ ignoreFirstNodeHighestBlockComment && isSortedNodeFirstNode,
+ }),
+ )
+ let sortedNodeText = sourceCode.getText(sortedNode)
+ let tokensAfter = sourceCode.getTokensAfter(node, {
+ includeComments: false,
+ count: 1,
+ })
+ let nextToken = tokensAfter.at(0)
+
+ let sortedNextNodeEndsWithSafeCharacter =
+ sortedNodeText.endsWith(';') || sortedNodeText.endsWith(',')
+ let isNextTokenOnSameLineAsNode = nextToken?.loc.start.line === node.loc.end.line
+ let isNextTokenSafeCharacter = nextToken?.value === ';' || nextToken?.value === ','
+ if (
+ isNextTokenOnSameLineAsNode &&
+ !sortedNextNodeEndsWithSafeCharacter &&
+ !isNextTokenSafeCharacter
+ ) {
+ sortedNodeCode += ';'
+ }
+
+ fixes.push(
+ fixer.replaceTextRange(
+ getNodeRange(node, sourceCode, {
+ ignoreHighestBlockComment:
+ ignoreFirstNodeHighestBlockComment && isNodeFirstNode,
+ }),
+ sortedNodeCode,
+ ),
+ )
+ fixes = [
+ ...fixes,
+ ...makeCommentAfterFixes({
+ fixer,
+ node,
+ sortedNode,
+ sourceCode,
+ }),
+ ]
+ }
+
+ return fixes
+}
diff --git a/src/utils/make-newline-fixes.ts b/src/utils/make-newline-fixes.ts
new file mode 100644
index 0000000..0f5f9c5
--- /dev/null
+++ b/src/utils/make-newline-fixes.ts
@@ -0,0 +1,95 @@
+import type { TSESLint } from '@typescript-eslint/utils'
+
+import { IMPORT_GROUPS } from '../rules/order.js'
+import { getGroupNumber } from './get-group-number.js'
+import { getLinesBetween } from './get-lines-between.js'
+import { getNodeRange } from './get-node-range.js'
+import type { SortingNode } from './types.js'
+
+interface MakeNewlineFixesParameters {
+ fixer: TSESLint.RuleFixer
+ nodes: SortingNode[]
+ sortedNodes: SortingNode[]
+ sourceCode: TSESLint.SourceCode
+ options: {
+ newlinesBetween: 'ignore' | 'always' | 'never'
+ }
+}
+
+export function makeNewlineFixes({
+ fixer,
+ nodes,
+ sortedNodes,
+ sourceCode,
+ options,
+}: MakeNewlineFixesParameters): TSESLint.RuleFix[] {
+ let fixes: TSESLint.RuleFix[] = []
+
+ for (let max = sortedNodes.length, index = 0; index < max; index++) {
+ let sortingNode = sortedNodes.at(index)!
+ let nextSortingNode = sortedNodes.at(index + 1)
+
+ if (options.newlinesBetween === 'ignore' || !nextSortingNode) {
+ continue
+ }
+
+ let nodeGroupNumber = getGroupNumber(IMPORT_GROUPS, sortingNode)
+ let nextNodeGroupNumber = getGroupNumber(IMPORT_GROUPS, nextSortingNode)
+ let currentNodeRange = getNodeRange(nodes.at(index)!.node, sourceCode)
+ let nextNodeRangeStart = getNodeRange(nodes.at(index + 1)!.node, sourceCode).at(0)!
+ let rangeToReplace: [number, number] = [currentNodeRange.at(1)!, nextNodeRangeStart]
+ let textBetweenNodes = sourceCode.text.slice(currentNodeRange.at(1), nextNodeRangeStart)
+
+ let linesBetweenMembers = getLinesBetween(
+ sourceCode,
+ nodes.at(index)!,
+ nodes.at(index + 1)!,
+ )
+
+ let rangeReplacement: undefined | string
+ if (
+ (options.newlinesBetween === 'always' &&
+ nodeGroupNumber === nextNodeGroupNumber &&
+ linesBetweenMembers !== 0) ||
+ (options.newlinesBetween === 'never' && linesBetweenMembers > 0)
+ ) {
+ rangeReplacement = getStringWithoutInvalidNewlines(textBetweenNodes)
+ }
+
+ if (
+ options.newlinesBetween === 'always' &&
+ nodeGroupNumber !== nextNodeGroupNumber &&
+ linesBetweenMembers !== 1
+ ) {
+ rangeReplacement = addNewlineBeforeFirstNewline(
+ linesBetweenMembers > 1
+ ? getStringWithoutInvalidNewlines(textBetweenNodes)
+ : textBetweenNodes,
+ )
+ let isOnSameLine =
+ linesBetweenMembers === 0 &&
+ nodes.at(index)!.node.loc.end.line === nodes.at(index + 1)!.node.loc.start.line
+ if (isOnSameLine) {
+ rangeReplacement = addNewlineBeforeFirstNewline(rangeReplacement)
+ }
+ }
+
+ if (rangeReplacement) {
+ fixes.push(fixer.replaceTextRange(rangeToReplace, rangeReplacement))
+ }
+ }
+
+ return fixes
+}
+
+function getStringWithoutInvalidNewlines(value: string): string {
+ return value.replaceAll(/\n\s*\n/gu, '\n').replaceAll(/\n+/gu, '\n')
+}
+
+function addNewlineBeforeFirstNewline(value: string): string {
+ let firstNewlineIndex = value.indexOf('\n')
+
+ if (firstNewlineIndex === -1) return `${value}\n`
+
+ return `${value.slice(0, firstNewlineIndex)}\n${value.slice(firstNewlineIndex)}`
+}
diff --git a/src/utils/range-to-diff.ts b/src/utils/range-to-diff.ts
new file mode 100644
index 0000000..0cb4c5b
--- /dev/null
+++ b/src/utils/range-to-diff.ts
@@ -0,0 +1,9 @@
+import type { TSESLint, TSESTree } from '@typescript-eslint/utils'
+
+export function rangeToDiff(node: TSESTree.Node, sourceCode: TSESLint.SourceCode): number {
+ let nodeText = sourceCode.getText(node)
+ let hasTrailingCommaOrSemicolon = nodeText.endsWith(';') || nodeText.endsWith(',')
+ let [from, to] = node.range
+
+ return to - from - (hasTrailingCommaOrSemicolon ? 1 : 0)
+}
diff --git a/src/utils/sort-nodes-by-groups.ts b/src/utils/sort-nodes-by-groups.ts
new file mode 100644
index 0000000..a0b5b9e
--- /dev/null
+++ b/src/utils/sort-nodes-by-groups.ts
@@ -0,0 +1,64 @@
+import { IMPORT_GROUPS } from '../rules/order.js'
+import type { CompareOptions } from './compare.js'
+import { getGroupNumber } from './get-group-number.js'
+import { sortNodes } from './sort-nodes.js'
+import type { SortingNode } from './types.js'
+
+interface ExtraOptions {
+ /**
+ * If not provided, `options` will be used. If function returns undefined,
+ * nodes will not be sorted within the group.
+ */
+ getGroupCompareOptions?(groupNumber: number): CompareOptions | undefined
+ // eslint-disable-next-line @typescript-eslint/member-ordering
+ ignoreEslintDisabledNodes?: boolean
+ isNodeIgnored?(node: T): boolean
+}
+
+export function sortNodesByGroups(
+ nodes: T[],
+ options: CompareOptions,
+ extraOptions?: ExtraOptions,
+): T[] {
+ let nodesByNonIgnoredGroupNumber: Record = {}
+ let ignoredNodeIndices: number[] = []
+
+ for (let [index, sortingNode] of nodes.entries()) {
+ if (
+ (sortingNode.isEslintDisabled && extraOptions?.ignoreEslintDisabledNodes) ??
+ extraOptions?.isNodeIgnored?.(sortingNode)
+ ) {
+ ignoredNodeIndices.push(index)
+ continue
+ }
+
+ let groupNumber = getGroupNumber(IMPORT_GROUPS, sortingNode)
+ nodesByNonIgnoredGroupNumber[groupNumber] ??= []
+ nodesByNonIgnoredGroupNumber[groupNumber].push(sortingNode)
+ }
+
+ let sortedNodes: T[] = []
+ for (let groupNumber of Object.keys(nodesByNonIgnoredGroupNumber).sort(
+ (a, b) => Number(a) - Number(b),
+ )) {
+ let compareOptions = extraOptions?.getGroupCompareOptions
+ ? extraOptions.getGroupCompareOptions(Number(groupNumber))
+ : options
+
+ if (!compareOptions) {
+ sortedNodes.push(...nodesByNonIgnoredGroupNumber[Number(groupNumber)])
+ continue
+ }
+
+ sortedNodes.push(
+ ...sortNodes(nodesByNonIgnoredGroupNumber[Number(groupNumber)], compareOptions),
+ )
+ }
+
+ // Add ignored nodes at the same position as they were before linting.
+ for (let ignoredIndex of ignoredNodeIndices) {
+ sortedNodes.splice(ignoredIndex, 0, nodes[ignoredIndex])
+ }
+
+ return sortedNodes
+}
diff --git a/src/utils/sort-nodes.ts b/src/utils/sort-nodes.ts
index c5ae299..d5691b4 100644
--- a/src/utils/sort-nodes.ts
+++ b/src/utils/sort-nodes.ts
@@ -1,6 +1,32 @@
import { compare, type CompareOptions } from './compare.js'
import type { SortingNode } from './types.js'
-export function sortNodes(nodes: T[], options: CompareOptions): T[] {
- return [...nodes].sort((a, b) => compare(a, b, options))
+interface ExtraOptions {
+ ignoreEslintDisabledNodes?: boolean
+}
+
+export function sortNodes(
+ nodes: T[],
+ options: CompareOptions,
+ extraOptions?: ExtraOptions,
+): T[] {
+ let nonIgnoredNodes: T[] = []
+ let ignoredNodeIndices: number[] = []
+
+ for (let [index, sortingNode] of nodes.entries()) {
+ if (sortingNode.isEslintDisabled && extraOptions?.ignoreEslintDisabledNodes) {
+ ignoredNodeIndices.push(index)
+ } else {
+ nonIgnoredNodes.push(sortingNode)
+ }
+ }
+
+ let sortedNodes = [...nonIgnoredNodes].sort((a, b) => compare(a, b, options))
+
+ // Add ignored nodes at the same position as they were before linting.
+ for (let ignoredIndex of ignoredNodeIndices) {
+ sortedNodes.splice(ignoredIndex, 0, nodes[ignoredIndex])
+ }
+
+ return sortedNodes
}
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 68fb783..cd1f969 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -10,11 +10,17 @@ export type ImportDeclarationNode =
export interface SortingNode {
name: string
node: Node
+ size: number
dependencies?: string[]
group?: string
+ isEslintDisabled: boolean
hasMultipleImportDeclarations?: boolean
}
+export interface MemberSortingNode extends SortingNode {
+ groupKind: 'value' | 'type'
+}
+
export type ImportGroup = (typeof IMPORT_GROUPS)[number]
export type ImportGroups = typeof IMPORT_GROUPS
diff --git a/tests/rules/order.test.ts b/tests/rules/order.test.ts
index e283556..b55c677 100644
--- a/tests/rules/order.test.ts
+++ b/tests/rules/order.test.ts
@@ -51,10 +51,7 @@ describe('order', () => {
errors: [
{
messageId: 'out-of-order',
- data: {
- left: 'b',
- right: 'a',
- },
+ data: { left: 'b', right: 'a' },
},
],
},
@@ -280,7 +277,13 @@ describe('order', () => {
import { a } from 'a'
import { c } from 'c'
`,
- errors: [{ messageId: 'out-of-order' }, { messageId: 'out-of-order' }],
+ errors: [
+ {
+ data: { left: 'd', right: 'b' },
+ messageId: 'out-of-order',
+ },
+ { messageId: 'out-of-order' },
+ ],
},
],
})
@@ -342,5 +345,21 @@ describe('order', () => {
},
],
})
+
+ ruleTester.run('ignores dynamic requires', orderRule, {
+ valid: [
+ {
+ name: 'without errors',
+ code: dedent`
+ const path = require('path')
+
+ const myFilename = require('the-filename')
+ const file = require(path.join(myDir, myFilename))
+ const other = require('./other.js')
+ `,
+ },
+ ],
+ invalid: [],
+ })
})
})
diff --git a/tests/rules/specifier-order.test.ts b/tests/rules/specifier-order.test.ts
new file mode 100644
index 0000000..dc5837f
--- /dev/null
+++ b/tests/rules/specifier-order.test.ts
@@ -0,0 +1,200 @@
+import { RuleTester } from '@typescript-eslint/rule-tester'
+import dedent from 'dedent'
+import { afterAll, describe, it } from 'vitest'
+
+import rule from '../../src/rules/specifier-order.js'
+
+describe('specifier-order', () => {
+ RuleTester.describeSkip = describe.skip
+ RuleTester.afterAll = afterAll
+ RuleTester.describe = describe
+ RuleTester.itOnly = it.only
+ RuleTester.itSkip = it.skip
+ RuleTester.it = it
+
+ let ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ sourceType: 'module',
+ ecmaVersion: 'latest',
+ },
+ // Use this after upgrade to eslint@9.
+ // languageOptions: { ecmaVersion: 'latest', sourceType: 'module' },
+ settings: {
+ 'import-sorting/framework-patterns': [/^react(\/|-dom|-router|$)/.source, 'prop-types'],
+ 'import-sorting/internal-patterns': /^~/.source,
+ },
+ })
+
+ describe('sorting by natural order', () => {
+ ruleTester.run('sorts specifiers', rule, {
+ valid: [
+ {
+ name: 'without errors',
+ code: dedent`
+ import { a, bb, ccc } from 'package'
+ `,
+ },
+ ],
+ invalid: [
+ {
+ name: 'fixes import order',
+ code: dedent`
+ import { bb, a, ccc } from 'package'
+ `,
+ output: dedent`
+ import { a, bb, ccc } from 'package'
+ `,
+ errors: [
+ {
+ messageId: 'specifier-out-of-order',
+ data: { left: 'bb', right: 'a' },
+ },
+ ],
+ },
+ ],
+ })
+
+ ruleTester.run('sorts specifiers on multiple lines', rule, {
+ valid: [
+ {
+ name: 'without errors',
+ code: dedent`
+ import {
+ a,
+ bb,
+ ccc,
+ dddd,
+ } from 'package'
+ `,
+ },
+ ],
+ invalid: [
+ {
+ name: 'fixes member order',
+ code: dedent`
+ import {
+ a,
+ dddd,
+ bb,
+ ccc,
+ } from 'package'
+ `,
+ output: dedent`
+ import {
+ a,
+ bb,
+ ccc,
+ dddd,
+ } from 'package'
+ `,
+ errors: [
+ {
+ messageId: 'specifier-out-of-order',
+ data: { left: 'dddd', right: 'bb' },
+ },
+ ],
+ },
+ ],
+ })
+
+ ruleTester.run('sorts aliased specifiers', rule, {
+ valid: [
+ {
+ name: 'without errors',
+ code: dedent`
+ import {
+ a as z,
+ bb as y,
+ ccc,
+ } from 'package'
+ `,
+ },
+ ],
+ invalid: [
+ {
+ name: 'fixes member order',
+ code: dedent`
+ import {
+ ccc,
+ bb as y,
+ a as z,
+ } from 'package'
+ `,
+ output: dedent`
+ import {
+ a as z,
+ bb as y,
+ ccc,
+ } from 'package'
+ `,
+ errors: [
+ {
+ messageId: 'specifier-out-of-order',
+ data: { left: 'ccc', right: 'bb' },
+ },
+ {
+ messageId: 'specifier-out-of-order',
+ data: { left: 'bb', right: 'a' },
+ },
+ ],
+ },
+ ],
+ })
+
+ ruleTester.run('does not sort default specifiers', rule, {
+ valid: [
+ {
+ name: 'without errors',
+ code: dedent`
+ import C, { b as A } from 'package'
+ `,
+ },
+ ],
+ invalid: [],
+ })
+
+ ruleTester.run('groups specifiers by kind', rule, {
+ valid: [
+ {
+ name: 'without errors',
+ code: dedent`
+ import {
+ a,
+ type bb,
+ bb,
+ type ccc,
+ } from 'package'
+ `,
+ },
+ ],
+ invalid: [
+ {
+ name: 'fixes member order',
+ code: dedent`
+ import {
+ a,
+ type bb,
+ type ccc,
+ bb,
+ } from 'package'
+ `,
+ output: dedent`
+ import {
+ a,
+ type bb,
+ bb,
+ type ccc,
+ } from 'package'
+ `,
+ errors: [
+ {
+ messageId: 'specifier-out-of-order',
+ data: { left: 'ccc', right: 'bb' },
+ },
+ ],
+ },
+ ],
+ })
+ })
+})