Skip to content

Commit 4ab4482

Browse files
authored
feat: add allowSeparateTypeImports option to no-duplicate-imports (#19872)
* feat: add allowSeparateTypeImports option to no-duplicate-imports * fix CI * clarify docs
1 parent 3fbcd70 commit 4ab4482

File tree

4 files changed

+533
-8
lines changed

4 files changed

+533
-8
lines changed

‎docs/src/rules/no-duplicate-imports.md

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ import * as something from 'module';
6161

6262
## Options
6363

64-
This rule takes one optional argument, an object with a single key, `includeExports` which is a `boolean`. It defaults to `false`.
64+
This rule has an object option:
65+
66+
* `"includeExports"`: `true` (default `false`) checks for exports in addition to imports.
67+
* `"allowSeparateTypeImports"`: `true` (default `false`) allows a type import alongside a value import from the same module in TypeScript files.
68+
69+
### includeExports
6570

6671
If re-exporting from an imported module, you should add the imports to the `import`-statement, and export that directly, not use `export ... from`.
6772

@@ -110,3 +115,59 @@ export * from 'module';
110115
```
111116

112117
:::
118+
119+
### allowSeparateTypeImports
120+
121+
TypeScript allows importing types using `import type`. By default, this rule flags instances of `import type` that have the same specifier as `import`. The `allowSeparateTypeImports` option allows you to override this behavior.
122+
123+
Example of **incorrect** TypeScript code for this rule with the default `{ "allowSeparateTypeImports": false }` option:
124+
125+
::: incorrect
126+
127+
```ts
128+
/*eslint no-duplicate-imports: ["error", { "allowSeparateTypeImports": false }]*/
129+
130+
import { someValue } from 'module';
131+
import type { SomeType } from 'module';
132+
```
133+
134+
:::
135+
136+
Example of **correct** TypeScript code for this rule with the default `{ "allowSeparateTypeImports": false }` option:
137+
138+
::: correct
139+
140+
```ts
141+
/*eslint no-duplicate-imports: ["error", { "allowSeparateTypeImports": false }]*/
142+
143+
import { someValue, type SomeType } from 'module';
144+
```
145+
146+
:::
147+
148+
Example of **incorrect** TypeScript code for this rule with the `{ "allowSeparateTypeImports": true }` option:
149+
150+
::: incorrect
151+
152+
```ts
153+
/*eslint no-duplicate-imports: ["error", { "allowSeparateTypeImports": true }]*/
154+
155+
import { someValue } from 'module';
156+
import type { SomeType } from 'module';
157+
import type { AnotherType } from 'module';
158+
```
159+
160+
:::
161+
162+
Example of **correct** TypeScript code for this rule with the `{ "allowSeparateTypeImports": true }` option:
163+
164+
::: correct
165+
166+
```ts
167+
/*eslint no-duplicate-imports: ["error", { "allowSeparateTypeImports": true }]*/
168+
169+
import { someValue } from 'module';
170+
import type { SomeType, AnotherType } from 'module';
171+
```
172+
173+
:::

‎lib/rules/no-duplicate-imports.js

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,33 @@ function isImportExportCanBeMerged(node1, node2) {
9191
* Returns a boolean if we should report (import|export).
9292
* @param {ASTNode} node A node to be reported or not.
9393
* @param {[ASTNode]} previousNodes An array contains previous nodes of the module imported or exported.
94+
* @param {boolean} allowSeparateTypeImports Whether to allow separate type and value imports.
9495
* @returns {boolean} True if the (import|export) should be reported.
9596
*/
96-
function shouldReportImportExport(node, previousNodes) {
97+
function shouldReportImportExport(
98+
node,
99+
previousNodes,
100+
allowSeparateTypeImports,
101+
) {
97102
let i = 0;
98103

99104
while (i < previousNodes.length) {
100-
if (isImportExportCanBeMerged(node, previousNodes[i])) {
105+
const previousNode = previousNodes[i];
106+
107+
if (allowSeparateTypeImports) {
108+
const isTypeNode =
109+
node.importKind === "type" || node.exportKind === "type";
110+
const isTypePrevious =
111+
previousNode.importKind === "type" ||
112+
previousNode.exportKind === "type";
113+
114+
if (isTypeNode !== isTypePrevious) {
115+
i++;
116+
continue;
117+
}
118+
}
119+
120+
if (isImportExportCanBeMerged(node, previousNode)) {
101121
return true;
102122
}
103123
i++;
@@ -136,6 +156,7 @@ function getModule(node) {
136156
* @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type.
137157
* @param {string} declarationType A declaration type can be an import or export.
138158
* @param {boolean} includeExports Whether or not to check for exports in addition to imports.
159+
* @param {boolean} allowSeparateTypeImports Whether to allow separate type and value imports.
139160
* @returns {void} No return value.
140161
*/
141162
function checkAndReport(
@@ -144,6 +165,7 @@ function checkAndReport(
144165
modules,
145166
declarationType,
146167
includeExports,
168+
allowSeparateTypeImports,
147169
) {
148170
const module = getModule(node);
149171

@@ -157,19 +179,43 @@ function checkAndReport(
157179
exportNodes = getNodesByDeclarationType(previousNodes, "export");
158180
}
159181
if (declarationType === "import") {
160-
if (shouldReportImportExport(node, importNodes)) {
182+
if (
183+
shouldReportImportExport(
184+
node,
185+
importNodes,
186+
allowSeparateTypeImports,
187+
)
188+
) {
161189
messagesIds.push("import");
162190
}
163191
if (includeExports) {
164-
if (shouldReportImportExport(node, exportNodes)) {
192+
if (
193+
shouldReportImportExport(
194+
node,
195+
exportNodes,
196+
allowSeparateTypeImports,
197+
)
198+
) {
165199
messagesIds.push("importAs");
166200
}
167201
}
168202
} else if (declarationType === "export") {
169-
if (shouldReportImportExport(node, exportNodes)) {
203+
if (
204+
shouldReportImportExport(
205+
node,
206+
exportNodes,
207+
allowSeparateTypeImports,
208+
)
209+
) {
170210
messagesIds.push("export");
171211
}
172-
if (shouldReportImportExport(node, importNodes)) {
212+
if (
213+
shouldReportImportExport(
214+
node,
215+
importNodes,
216+
allowSeparateTypeImports,
217+
)
218+
) {
173219
messagesIds.push("exportAs");
174220
}
175221
}
@@ -196,13 +242,15 @@ function checkAndReport(
196242
* @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type.
197243
* @param {string} declarationType A declaration type can be an import or export.
198244
* @param {boolean} includeExports Whether or not to check for exports in addition to imports.
245+
* @param {boolean} allowSeparateTypeImports Whether to allow separate type and value imports.
199246
* @returns {nodeCallback} A function passed to ESLint to handle the statement.
200247
*/
201248
function handleImportsExports(
202249
context,
203250
modules,
204251
declarationType,
205252
includeExports,
253+
allowSeparateTypeImports,
206254
) {
207255
return function (node) {
208256
const module = getModule(node);
@@ -214,6 +262,7 @@ function handleImportsExports(
214262
modules,
215263
declarationType,
216264
includeExports,
265+
allowSeparateTypeImports,
217266
);
218267
const currentNode = { node, declarationType };
219268
let nodes = [currentNode];
@@ -231,11 +280,14 @@ function handleImportsExports(
231280
/** @type {import('../types').Rule.RuleModule} */
232281
module.exports = {
233282
meta: {
283+
dialects: ["javascript", "typescript"],
284+
language: "javascript",
234285
type: "problem",
235286

236287
defaultOptions: [
237288
{
238289
includeExports: false,
290+
allowSeparateTypeImports: false,
239291
},
240292
],
241293

@@ -252,6 +304,9 @@ module.exports = {
252304
includeExports: {
253305
type: "boolean",
254306
},
307+
allowSeparateTypeImports: {
308+
type: "boolean",
309+
},
255310
},
256311
additionalProperties: false,
257312
},
@@ -266,14 +321,15 @@ module.exports = {
266321
},
267322

268323
create(context) {
269-
const [{ includeExports }] = context.options;
324+
const [{ includeExports, allowSeparateTypeImports }] = context.options;
270325
const modules = new Map();
271326
const handlers = {
272327
ImportDeclaration: handleImportsExports(
273328
context,
274329
modules,
275330
"import",
276331
includeExports,
332+
allowSeparateTypeImports,
277333
),
278334
};
279335

@@ -283,12 +339,14 @@ module.exports = {
283339
modules,
284340
"export",
285341
includeExports,
342+
allowSeparateTypeImports,
286343
);
287344
handlers.ExportAllDeclaration = handleImportsExports(
288345
context,
289346
modules,
290347
"export",
291348
includeExports,
349+
allowSeparateTypeImports,
292350
);
293351
}
294352
return handlers;

‎lib/types/rules.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2292,6 +2292,10 @@ export interface ESLintRules extends Linter.RulesRecord {
22922292
* @default false
22932293
*/
22942294
includeExports: boolean;
2295+
/**
2296+
* @default false
2297+
*/
2298+
allowSeparateTypeImports: boolean;
22952299
}>,
22962300
]
22972301
>;

0 commit comments

Comments
 (0)