fix(linter/import): import/no-duplicates handles namespace imports correctly (#6694)

Closes #6660

- Value (i.e. not `import type`) namespace imports are now grouped correctly
- When grouping, consider _all_ import entries, not just the first one. Fixes a false negative when inlined `type` imports are used, e.g. `import { a, type b } from 'foo'`
This commit is contained in:
DonIsaac 2024-10-20 00:20:59 +00:00
parent 23f88b3e18
commit a5de2309bc
2 changed files with 58 additions and 34 deletions

View file

@ -5,6 +5,7 @@ use oxc_diagnostics::{LabeledSpan, OxcDiagnostic};
use oxc_macros::declare_oxc_lint; use oxc_macros::declare_oxc_lint;
use oxc_span::Span; use oxc_span::Span;
use oxc_syntax::module_record::{ImportImportName, RequestedModule}; use oxc_syntax::module_record::{ImportImportName, RequestedModule};
use rustc_hash::FxHashMap;
use crate::{context::LintContext, rule::Rule}; use crate::{context::LintContext, rule::Rule};
@ -55,11 +56,20 @@ declare_oxc_lint!(
/// ```javascript /// ```javascript
/// import { foo } from './module'; /// import { foo } from './module';
/// import { bar } from './module'; /// import { bar } from './module';
///
/// import a from './module';
/// import { b } from './module';
/// ``` /// ```
/// ///
/// Examples of **correct** code for this rule: /// Examples of **correct** code for this rule:
/// ```javascript /// ```typescript
/// import { foo, bar } from './module'; /// import { foo, bar } from './module';
///
/// import * as a from 'foo'; // separate statements for namespace imports
/// import { b } from 'foo';
///
/// import { c } from 'foo'; // separate type imports, unless
/// import type { d } from 'foo'; // `preferInline` is true
/// ``` /// ```
NoDuplicates, NoDuplicates,
suspicious suspicious
@ -91,33 +101,39 @@ impl Rule for NoDuplicates {
.chunk_by(|r| r.0.clone()); .chunk_by(|r| r.0.clone());
for (_path, group) in &groups { for (_path, group) in &groups {
let has_type_import = module_record.import_entries.iter().any(|entry| entry.is_type); let requested_modules = group
// When prefer_inline is false, 0 is value, 1 is type named, 2 is type namespace and 3 is type default
// When prefer_inline is true, 0 is value and type named, 2 is type namespace and 3 is type default
let import_entries_maps = group
.into_iter() .into_iter()
.flat_map(|(_path, requested_modules)| requested_modules) .flat_map(|(_path, requested_modules)| requested_modules)
.filter(|requested_module| requested_module.is_import()) .filter(|requested_module| requested_module.is_import());
.into_group_map_by(|requested_module| { // When prefer_inline is false, 0 is value, 1 is type named, 2 is type namespace and 3 is type default
// We should early return if there is no type import // When prefer_inline is true, 0 is value and type named, 2 is type // namespace and 3 is type default
if !has_type_import { let mut import_entries_maps: FxHashMap<i8, Vec<&RequestedModule>> =
return 0; FxHashMap::default();
}; for requested_module in requested_modules {
for entry in &module_record.import_entries { let imports = module_record
if entry.module_request.span() != requested_module.span() { .import_entries
continue; .iter()
.filter(|entry| entry.module_request.span() == requested_module.span())
.collect::<Vec<_>>();
if imports.is_empty() {
import_entries_maps.entry(0).or_default().push(requested_module);
} }
for imports in imports {
if entry.is_type { let key = if imports.is_type {
return match entry.import_name { match imports.import_name {
ImportImportName::Name(_) => i8::from(!self.prefer_inline), ImportImportName::Name(_) => i8::from(!self.prefer_inline),
ImportImportName::NamespaceObject => 2, ImportImportName::NamespaceObject => 2,
ImportImportName::Default(_) => 3, ImportImportName::Default(_) => 3,
}
} else {
match imports.import_name {
ImportImportName::NamespaceObject => 2,
_ => 0,
}
}; };
import_entries_maps.entry(key).or_default().push(requested_module);
} }
} }
0
});
for i in 0..4 { for i in 0..4 {
check_duplicates(ctx, import_entries_maps.get(&i)); check_duplicates(ctx, import_entries_maps.get(&i));
@ -147,13 +163,13 @@ fn test() {
(r#"import "./malformed.js""#, None), (r#"import "./malformed.js""#, None),
(r"import { x } from './foo'; import { y } from './bar'", None), (r"import { x } from './foo'; import { y } from './bar'", None),
(r#"import foo from "234artaf"; import { shoop } from "234q25ad""#, None), (r#"import foo from "234artaf"; import { shoop } from "234q25ad""#, None),
// r#"import { x } from './foo'; import type { y } from './foo'"#, (r"import { x } from './foo'; import type { y } from './foo'", None),
// TODO: considerQueryString // TODO: considerQueryString
// r#"import x from './bar?optionX'; import y from './bar?optionY';"#, // r#"import x from './bar?optionX'; import y from './bar?optionY';"#,
(r"import x from './foo'; import y from './bar';", None), (r"import x from './foo'; import y from './bar';", None),
// TODO: separate namespace // TODO: separate namespace
// (r"import * as ns from './foo'; import { y } from './foo'", None), (r"import * as ns from './foo'; import { y } from './foo'", None),
// (r"import { y } from './foo'; import * as ns from './foo'", None), (r"import { y } from './foo'; import * as ns from './foo'", None),
// TypeScript // TypeScript
(r"import type { x } from './foo'; import y from './foo'", None), (r"import type { x } from './foo'; import y from './foo'", None),
(r"import type x from './foo'; import type y from './bar'", None), (r"import type x from './foo'; import type y from './bar'", None),

View file

@ -209,7 +209,7 @@ source: crates/oxc_linter/src/tester.rs
⚠ eslint-plugin-import(no-duplicates): Module './foo' is imported more than once in this file ⚠ eslint-plugin-import(no-duplicates): Module './foo' is imported more than once in this file
╭─[index.ts:1:8] ╭─[index.ts:1:8]
1 │ import './foo'; import def, {x} from './foo' 1 │ import './foo'; import def, {x} from './foo'
· ───┬─── ─────── · ───┬─── ───────
· ╰── It is first imported here · ╰── It is first imported here
╰──── ╰────
help: Merge these imports into a single import statement help: Merge these imports into a single import statement
@ -249,7 +249,7 @@ source: crates/oxc_linter/src/tester.rs
⚠ eslint-plugin-import(no-duplicates): Module './foo' is imported more than once in this file ⚠ eslint-plugin-import(no-duplicates): Module './foo' is imported more than once in this file
╭─[index.ts:1:17] ╭─[index.ts:1:17]
1 │ import {x} from './foo'; import def, {y} from './foo' 1 │ import {x} from './foo'; import def, {y} from './foo'
· ───┬─── ─────── · ───┬─── ───────
· ╰── It is first imported here · ╰── It is first imported here
╰──── ╰────
help: Merge these imports into a single import statement help: Merge these imports into a single import statement
@ -263,9 +263,9 @@ source: crates/oxc_linter/src/tester.rs
help: Merge these imports into a single import statement help: Merge these imports into a single import statement
⚠ eslint-plugin-import(no-duplicates): Module './foo' is imported more than once in this file ⚠ eslint-plugin-import(no-duplicates): Module './foo' is imported more than once in this file
╭─[index.ts:1:21] ╭─[index.ts:1:46]
1 │ import * as ns from './foo'; import {x} from './foo'; import {y} from './foo' 1 │ import * as ns from './foo'; import {x} from './foo'; import {y} from './foo'
· ───┬─── ─────── ─────── · ───┬─── ───────
· ╰── It is first imported here · ╰── It is first imported here
╰──── ╰────
help: Merge these imports into a single import statement help: Merge these imports into a single import statement
@ -273,7 +273,7 @@ source: crates/oxc_linter/src/tester.rs
⚠ eslint-plugin-import(no-duplicates): Module './foo' is imported more than once in this file ⚠ eslint-plugin-import(no-duplicates): Module './foo' is imported more than once in this file
╭─[index.ts:1:17] ╭─[index.ts:1:17]
1 │ import {x} from './foo'; import * as ns from './foo'; import {y} from './foo'; import './foo' 1 │ import {x} from './foo'; import * as ns from './foo'; import {y} from './foo'; import './foo'
· ───┬─── ─────── ─────── ─────── · ───┬─── ─────── ───────
· ╰── It is first imported here · ╰── It is first imported here
╰──── ╰────
help: Merge these imports into a single import statement help: Merge these imports into a single import statement
@ -540,6 +540,14 @@ source: crates/oxc_linter/src/tester.rs
╰──── ╰────
help: Merge these imports into a single import statement help: Merge these imports into a single import statement
⚠ eslint-plugin-import(no-duplicates): Module './foo' is imported more than once in this file
╭─[index.ts:1:38]
1 │ import {AValue, type x, BValue} from './foo'; import {type y} from './foo'
· ───┬────
· ╰── It is first imported here
╰────
help: Merge these imports into a single import statement
⚠ eslint-plugin-import(no-duplicates): Module './foo' is imported more than once in this file ⚠ eslint-plugin-import(no-duplicates): Module './foo' is imported more than once in this file
╭─[index.ts:1:38] ╭─[index.ts:1:38]
1 │ import {AValue, type x, BValue} from './foo'; import {type y} from './foo' 1 │ import {AValue, type x, BValue} from './foo'; import {type y} from './foo'