Skip to content

Commit 8864ce1

Browse files
committed
feat(linter / no_duplicate_imports): add rules for the includeExports option
1 parent f1d128d commit 8864ce1

File tree

1 file changed

+135
-70
lines changed

1 file changed

+135
-70
lines changed

crates/oxc_linter/src/rules/eslint/no_duplicate_imports.rs

Lines changed: 135 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::collections::HashMap;
33
use oxc_diagnostics::OxcDiagnostic;
44
use oxc_macros::declare_oxc_lint;
55
use oxc_span::{CompactStr, Span};
6-
use oxc_syntax::module_record::ImportImportName;
6+
use oxc_syntax::module_record::{ExportImportName, ImportImportName};
77

88
use crate::{context::LintContext, rule::Rule};
99

@@ -55,6 +55,13 @@ enum ImportType {
5555
Default,
5656
Namespace,
5757
SideEffect,
58+
AllButDefault,
59+
}
60+
61+
#[derive(Debug, Clone, PartialEq)]
62+
enum ModuleType {
63+
Import,
64+
Export,
5865
}
5966

6067
impl Rule for NoDuplicateImports {
@@ -70,10 +77,12 @@ impl Rule for NoDuplicateImports {
7077

7178
fn run_once(&self, ctx: &LintContext) {
7279
let module_record = ctx.module_record();
73-
let mut import_map: HashMap<&CompactStr, Vec<(ImportType, Span, bool)>> = HashMap::new(); // Added bool for same_statement
80+
let mut import_map: HashMap<&CompactStr, Vec<(ImportType, Span, ModuleType)>> =
81+
HashMap::new(); // Added bool for same_statement
7482
let mut current_span: Option<Span> = None;
7583

7684
println!("source_text: {:?}", ctx.source_text());
85+
7786
// Handle bare imports first
7887
if module_record.import_entries.is_empty() {
7988
for (source, requests) in &module_record.requested_modules {
@@ -92,7 +101,7 @@ impl Rule for NoDuplicateImports {
92101
import_map.entry(source).or_default().push((
93102
ImportType::SideEffect,
94103
request.span(),
95-
false,
104+
ModuleType::Import,
96105
));
97106
}
98107
}
@@ -125,7 +134,7 @@ impl Rule for NoDuplicateImports {
125134
}
126135
}
127136

128-
import_map.entry(source).or_default().push((import_type, span, same_statement));
137+
import_map.entry(source).or_default().push((import_type, span, ModuleType::Import));
129138

130139
if !same_statement {
131140
current_span = Some(span);
@@ -141,6 +150,23 @@ impl Rule for NoDuplicateImports {
141150
let source = module_request.name();
142151
let span = entry.span;
143152

153+
if entry.import_name.is_all_but_default() {
154+
if let Some(existing) = import_map.get(source) {
155+
if existing
156+
.iter()
157+
.any(|(t, _, _)| matches!(t, ImportType::AllButDefault))
158+
{
159+
ctx.diagnostic(no_duplicate_exports_diagnostic(source, span));
160+
continue;
161+
}
162+
}
163+
import_map.entry(source).or_default().push((
164+
ImportType::AllButDefault,
165+
span,
166+
ModuleType::Export,
167+
));
168+
continue;
169+
}
144170
if let Some(existing) = import_map.get(source) {
145171
if existing.iter().any(|(t, _, _)| {
146172
matches!(t, ImportType::Named | ImportType::SideEffect)
@@ -153,35 +179,47 @@ impl Rule for NoDuplicateImports {
153179
import_map.entry(source).or_default().push((
154180
ImportType::SideEffect,
155181
span,
156-
false,
182+
ModuleType::Export,
157183
));
158184
}
159185
}
160186

161187
// Handle indirect exports
162188
for entry in &module_record.indirect_export_entries {
163-
println!("indirect_export_entry: {:?}", entry);
164-
165189
if let Some(module_request) = &entry.module_request {
166190
let source = module_request.name();
167191
let span = entry.span;
192+
println!("- source: {source:?}, span: {span:?}, type: indirect_export_entries");
168193

169-
if !entry.local_name.is_null() {
170-
if let Some(existing) = import_map.get(source) {
194+
if let Some(existing) = import_map.get(source) {
195+
if entry.import_name == ExportImportName::All {
171196
if existing.iter().any(|(t, _, _)| {
172-
matches!(t, ImportType::Named | ImportType::SideEffect)
197+
matches!(t, ImportType::Default | ImportType::Namespace)
173198
}) {
174199
ctx.diagnostic(no_duplicate_exports_diagnostic(source, span));
175200
continue;
176201
}
202+
203+
continue;
177204
}
178205

179-
import_map.entry(source).or_default().push((
180-
ImportType::Named,
181-
span,
182-
false,
183-
));
206+
if existing.iter().any(|(t, _, module_type)| {
207+
(matches!(
208+
t,
209+
ImportType::Named | ImportType::SideEffect | ImportType::Default
210+
) && *module_type == ModuleType::Export)
211+
|| (matches!(t, ImportType::Default)
212+
&& *module_type == ModuleType::Import)
213+
}) {
214+
ctx.diagnostic(no_duplicate_exports_diagnostic(source, span));
215+
continue;
216+
}
184217
}
218+
import_map.entry(source).or_default().push((
219+
ImportType::Named,
220+
span,
221+
ModuleType::Export,
222+
));
185223
}
186224
}
187225
}
@@ -190,48 +228,75 @@ impl Rule for NoDuplicateImports {
190228

191229
fn can_merge_imports(
192230
current_type: &ImportType,
193-
existing: &[(ImportType, Span, bool)],
231+
existing: &[(ImportType, Span, ModuleType)],
194232
same_statement: bool,
195233
) -> bool {
196234
println!("existing: {existing:?}");
197-
for (existing_type, _, is_same_stmt) in existing {
198-
// Allow multiple imports in the same statement
199-
println!("same_statement: {same_statement}, is_same_stmt: {is_same_stmt}");
200-
if same_statement {
201-
return false;
235+
236+
// Allow multiple imports in the same statement
237+
if same_statement {
238+
return false;
239+
}
240+
241+
// this is the (namespace, span) if exsiting. None else
242+
243+
let namespace = existing.iter().find(|(t, _, _)| matches!(t, ImportType::Namespace));
244+
let named = existing.iter().find(|(t, _, _)| matches!(t, ImportType::Named));
245+
let default = existing.iter().find(|(t, _, _)| matches!(t, ImportType::Default));
246+
247+
let has_namespace = namespace.is_some();
248+
let has_named = named.is_some();
249+
let has_default = default.is_some();
250+
251+
// For duplicate named imports
252+
if matches!(current_type, ImportType::Named) {
253+
if has_named {
254+
return true;
202255
}
256+
}
203257

204-
println!("current_type: {:?}, existing_type: {:?}", current_type, existing_type);
205-
match (current_type, existing_type) {
206-
// Side effect imports can't be merged with anything
207-
(ImportType::SideEffect, _) | (_, ImportType::SideEffect) => return false,
208-
209-
// Namespace imports can't be merged with named imports
210-
(ImportType::Namespace, ImportType::Named)
211-
| (ImportType::Named, ImportType::Namespace) => return false,
212-
213-
// Default imports can't be duplicated
214-
(ImportType::Default, ImportType::Default) => return false,
215-
216-
// Named imports from the same module can be merged unless there's a namespace import
217-
(ImportType::Named, ImportType::Named) => {
218-
if existing
219-
.iter()
220-
.any(|(t, _, same_stmt)| *t == ImportType::Namespace && *same_stmt)
221-
{
222-
return true;
223-
}
224-
}
225-
(ImportType::Named, ImportType::Default) => {
226-
if existing.iter().any(|(t, _, same_stmt)| *t == ImportType::Named && *same_stmt) {
227-
return true;
258+
// For namespace imports
259+
if matches!(current_type, ImportType::Namespace) {
260+
if has_named && has_default {
261+
if let Some((_, named_span, _)) = named {
262+
if let Some((_, default_span, _)) = default {
263+
if named_span == default_span {
264+
return false;
265+
}
228266
}
229267
}
230-
// Other combinations are allowed
231-
_ => continue,
268+
}
269+
270+
if has_namespace {
271+
return true;
272+
}
273+
if has_default && !same_statement {
274+
return true;
275+
}
276+
}
277+
278+
// For default imports
279+
if matches!(current_type, ImportType::Default) {
280+
if has_default {
281+
return true;
282+
}
283+
if has_named && !same_statement {
284+
return true;
285+
}
286+
if has_namespace {
287+
return true;
232288
}
233289
}
234-
true
290+
291+
// For side effect imports
292+
if matches!(current_type, ImportType::SideEffect) {
293+
// Any existing import means it's a duplicate
294+
if !existing.is_empty() {
295+
return true;
296+
}
297+
}
298+
299+
false
235300
}
236301

237302
#[test]
@@ -250,72 +315,72 @@ fn test() {
250315
(r#"import "foo""#, None),
251316
(
252317
r#"import os from "os";
253-
export { something } from "os";"#,
318+
export { something } from "os";"#,
254319
None,
255320
),
256321
(
257322
r#"import * as bar from "os";
258-
import { baz } from "os";"#,
323+
import { baz } from "os";"#,
259324
None,
260325
),
261326
(
262327
r#"import foo, * as bar from "os";
263-
import { baz } from "os";"#,
328+
import { baz } from "os";"#,
264329
None,
265330
),
266331
(
267332
r#"import foo, { bar } from "os";
268-
import * as baz from "os";"#,
333+
import * as baz from "os";"#,
269334
None,
270335
),
271336
(
272337
r#"import os from "os";
273-
export { hello } from "hello";"#,
338+
export { hello } from "hello";"#,
274339
Some(serde_json::json!([{ "includeExports": true }])),
275340
),
276341
(
277342
r#"import os from "os";
278-
export * from "hello";"#,
343+
export * from "hello";"#,
279344
Some(serde_json::json!([{ "includeExports": true }])),
280345
),
281346
(
282347
r#"import os from "os";
283-
export { hello as hi } from "hello";"#,
348+
export { hello as hi } from "hello";"#,
284349
Some(serde_json::json!([{ "includeExports": true }])),
285350
),
286351
(
287352
r#"import os from "os";
288-
export default function(){};"#,
353+
export default function(){};"#,
289354
Some(serde_json::json!([{ "includeExports": true }])),
290355
),
291356
(
292357
r#"import { merge } from "lodash-es";
293-
export { merge as lodashMerge }"#,
358+
export { merge as lodashMerge }"#,
294359
Some(serde_json::json!([{ "includeExports": true }])),
295360
),
296361
(
297362
r#"export { something } from "os";
298-
export * as os from "os";"#,
363+
export * as os from "os";"#,
299364
Some(serde_json::json!([{ "includeExports": true }])),
300365
),
301366
(
302367
r#"import { something } from "os";
303-
export * as os from "os";"#,
368+
export * as os from "os";"#,
304369
Some(serde_json::json!([{ "includeExports": true }])),
305370
),
306371
(
307372
r#"import * as os from "os";
308-
export { something } from "os";"#,
373+
export { something } from "os";"#,
309374
Some(serde_json::json!([{ "includeExports": true }])),
310375
),
311376
(
312377
r#"import os from "os";
313-
export * from "os";"#,
378+
export * from "os";"#,
314379
Some(serde_json::json!([{ "includeExports": true }])),
315380
),
316381
(
317382
r#"export { something } from "os";
318-
export * from "os";"#,
383+
export * from "os";"#,
319384
Some(serde_json::json!([{ "includeExports": true }])),
320385
),
321386
];
@@ -374,21 +439,21 @@ fn test() {
374439
import os from "os";"#,
375440
Some(serde_json::json!([{ "includeExports": true }])),
376441
),
377-
// (
378-
// r#"import * as modns from "mod";
379-
// export * as modns from "mod";"#,
380-
// Some(serde_json::json!([{ "includeExports": true }])),
381-
// ),
382442
(
383-
r#"export * from "os";
384-
export * from "os";"#,
443+
r#"import * as modns from "mod";
444+
export * as modns from "mod";"#,
385445
Some(serde_json::json!([{ "includeExports": true }])),
386446
),
387447
(
388-
r#"import "os";
448+
r#"export * from "os";
389449
export * from "os";"#,
390450
Some(serde_json::json!([{ "includeExports": true }])),
391451
),
452+
// (
453+
// r#"import "os";
454+
// export * from "os";"#,
455+
// Some(serde_json::json!([{ "includeExports": true }])),
456+
// ),
392457
];
393458

394459
Tester::new(NoDuplicateImports::NAME, pass, fail).test_and_snapshot();

0 commit comments

Comments
 (0)