Skip to content

Commit

Permalink
feat(linter): add a config option to the no_duplicate_imports rule
Browse files Browse the repository at this point in the history
  • Loading branch information
Spoutnik97 committed Nov 17, 2024
1 parent 0f33813 commit 3cdc7fb
Show file tree
Hide file tree
Showing 2 changed files with 261 additions and 70 deletions.
232 changes: 179 additions & 53 deletions crates/oxc_linter/src/rules/eslint/no_duplicate_imports.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::collections::HashMap;

use oxc_ast::{
ast::{ImportDeclaration, ImportDeclarationSpecifier},
ast::{
ExportAllDeclaration, ExportNamedDeclaration, ImportDeclaration, ImportDeclarationSpecifier,
},
AstKind,
};
use oxc_diagnostics::OxcDiagnostic;
Expand All @@ -16,8 +18,16 @@ fn no_duplicate_imports_diagnostic(module_name: &str, span: Span) -> OxcDiagnost
.with_label(span)
}

fn no_duplicate_exports_diagnostic(module_name: &str, span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn(format!("'{}' export is duplicated", module_name))
.with_help("Merge the duplicated exports into a single export statement")
.with_label(span)
}

#[derive(Debug, Default, Clone)]
pub struct NoDuplicateImports {}
pub struct NoDuplicateImports {
include_exports: bool,
}

declare_oxc_lint!(
/// ### What it does
Expand All @@ -41,20 +51,40 @@ declare_oxc_lint!(
/// import something from 'another-module';
/// ```
NoDuplicateImports,
style,
nursery,
pending);

#[derive(Debug, Clone)]
enum DeclarationType {
Import,
Export,
}

#[derive(Debug, Clone)]
enum Specifier {
Named,
Default,
Namespace,
All,
}

#[derive(Debug, Clone)]
struct ModuleEntry {
specifier: Specifier,
declaration_type: DeclarationType,
}

impl Rule for NoDuplicateImports {
fn from_configuration(value: serde_json::Value) -> Self {
let Some(value) = value.get(0) else { return Self { include_exports: false } };
Self {
include_exports: value
.get("includeExports")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false),
}
}

fn run_once(&self, ctx: &LintContext) {
let semantic = ctx.semantic();
let nodes = semantic.nodes();
Expand All @@ -66,6 +96,12 @@ impl Rule for NoDuplicateImports {
AstKind::ImportDeclaration(import_decl) => {
handle_import(import_decl, &mut modules, ctx);
}
AstKind::ExportNamedDeclaration(export_decl) if self.include_exports => {
handle_export(export_decl, &mut modules, ctx);
}
AstKind::ExportAllDeclaration(export_decl) if self.include_exports => {
handle_export_all(export_decl, &mut modules, ctx);
}
_ => {}
}
}
Expand All @@ -79,30 +115,120 @@ fn handle_import(
) {
let source = &import_decl.source;
let module_name = source.value.to_string();
let mut specifier = Specifier::All;

if let Some(specifiers) = &import_decl.specifiers {
let has_namespace = specifiers.iter().any(|s| match s {
ImportDeclarationSpecifier::ImportDefaultSpecifier(_) => false,
ImportDeclarationSpecifier::ImportNamespaceSpecifier(_) => true,
_ => false,
});

specifier = if specifiers.iter().any(|s| match s {
ImportDeclarationSpecifier::ImportDefaultSpecifier(_) => true,
_ => false,
}) {
Specifier::Default
} else if specifiers.iter().any(|s| match s {
ImportDeclarationSpecifier::ImportNamespaceSpecifier(_) => true,
_ => false,
}) {
Specifier::Namespace
} else {
Specifier::Named
};

if has_namespace {
return;
}
}

if let Some(existing_modules) = modules.get(&module_name) {
if existing_modules
.iter()
.any(|entry| matches!(entry.declaration_type, DeclarationType::Import))
{
if existing_modules.iter().any(|entry| {
matches!(entry.declaration_type, DeclarationType::Import)
|| matches!(
(entry.declaration_type.clone(), entry.specifier.clone()),
(DeclarationType::Export, Specifier::All)
)
}) {
ctx.diagnostic(no_duplicate_imports_diagnostic(&module_name, import_decl.span));
return;
}
}

let entry = ModuleEntry { declaration_type: DeclarationType::Import };
let entry = ModuleEntry { declaration_type: DeclarationType::Import, specifier };
modules.entry(module_name.clone()).or_default().push(entry);
}

fn handle_export(
export_decl: &ExportNamedDeclaration,
modules: &mut HashMap<String, Vec<ModuleEntry>>,
ctx: &LintContext,
) {
let source = match &export_decl.source {
Some(source) => source,
None => return,
};
let module_name = source.value.to_string();

if let Some(existing_modules) = modules.get(&module_name) {
if existing_modules.iter().any(|entry| {
matches!(entry.declaration_type, DeclarationType::Export)
|| matches!(entry.declaration_type, DeclarationType::Import)
}) {
ctx.diagnostic(no_duplicate_exports_diagnostic(&module_name, export_decl.span));
}
}

modules.entry(module_name).or_default().push(ModuleEntry {
declaration_type: DeclarationType::Export,
specifier: Specifier::Named,
});
}

fn handle_export_all(
export_decl: &ExportAllDeclaration,
modules: &mut HashMap<String, Vec<ModuleEntry>>,
ctx: &LintContext,
) {
let source = &export_decl.source;
let module_name = source.value.to_string();

let exported_name = export_decl.exported.clone();

if let Some(existing_modules) = modules.get(&module_name) {
if existing_modules.iter().any(|entry| {
matches!(
(&entry.declaration_type, &entry.specifier),
(DeclarationType::Import, Specifier::All)
) || matches!(
(&entry.declaration_type, &entry.specifier),
(DeclarationType::Export, Specifier::All)
)
}) {
ctx.diagnostic(no_duplicate_exports_diagnostic(&module_name, export_decl.span));
}

if exported_name.is_none() {
return;
}

if existing_modules.iter().any(|entry| {
matches!(
(&entry.declaration_type, &entry.specifier),
(DeclarationType::Import, Specifier::Default)
)
}) {
ctx.diagnostic(no_duplicate_exports_diagnostic(&module_name, export_decl.span));
}
}

modules
.entry(module_name)
.or_default()
.push(ModuleEntry { declaration_type: DeclarationType::Export, specifier: Specifier::All });
}

#[test]
fn test() {
use crate::tester::Tester;
Expand Down Expand Up @@ -192,72 +318,72 @@ fn test() {
let fail = vec![
(
r#"import "fs";
import "fs""#,
import "fs""#,
None,
),
(
r#"import { merge } from "lodash-es";
import { find } from "lodash-es";"#,
import { find } from "lodash-es";"#,
None,
),
(
r#"import { merge } from "lodash-es";
import _ from "lodash-es";"#,
import _ from "lodash-es";"#,
None,
),
(
r#"import os from "os";
import { something } from "os";
import * as foobar from "os";"#,
import { something } from "os";
import * as foobar from "os";"#,
None,
),
(
r#"import * as modns from "lodash-es";
import { merge } from "lodash-es";
import { baz } from "lodash-es";"#,
import { merge } from "lodash-es";
import { baz } from "lodash-es";"#,
None,
),
(
r#"export { os } from "os";
export { something } from "os";"#,
Some(serde_json::json!([{ "includeExports": true }])),
),
(
r#"import os from "os";
export { os as foobar } from "os";
export { something } from "os";"#,
Some(serde_json::json!([{ "includeExports": true }])),
),
(
r#"import os from "os";
export { something } from "os";"#,
Some(serde_json::json!([{ "includeExports": true }])),
),
(
r#"import os from "os";
export * as os from "os";"#,
Some(serde_json::json!([{ "includeExports": true }])),
),
(
r#"export * as os from "os";
import os from "os";"#,
Some(serde_json::json!([{ "includeExports": true }])),
),
// (
// r#"export { os } from "os";
// export { something } from "os";"#,
// Some(serde_json::json!([{ "includeExports": true }])),
// ),
// (
// r#"import os from "os";
// export { os as foobar } from "os";
// export { something } from "os";"#,
// r#"import * as modns from "mod";
// export * as modns from "mod";"#,
// Some(serde_json::json!([{ "includeExports": true }])),
// ),
// (
// r#"import os from "os";
// export { something } from "os";"#,
// Some(serde_json::json!([{ "includeExports": true }])),
// ),
// (
// r#"import os from "os";
// export * as os from "os";"#,
// Some(serde_json::json!([{ "includeExports": true }])),
// ),
// (
// r#"export * as os from "os";
// import os from "os";"#,
// Some(serde_json::json!([{ "includeExports": true }])),
// ),
// (
// r#"import * as modns from "mod";
// export * as modns from "mod";"#,
// Some(serde_json::json!([{ "includeExports": true }])),
// ),
// (
// r#"export * from "os";
// export * from "os";"#,
// Some(serde_json::json!([{ "includeExports": true }])),
// ),
// (
// r#"import "os";
// export * from "os";"#,
// Some(serde_json::json!([{ "includeExports": true }])),
// ),
(
r#"export * from "os";
export * from "os";"#,
Some(serde_json::json!([{ "includeExports": true }])),
),
(
r#"import "os";
export * from "os";"#,
Some(serde_json::json!([{ "includeExports": true }])),
),
];

Tester::new(NoDuplicateImports::NAME, pass, fail).test_and_snapshot();
Expand Down
Loading

0 comments on commit 3cdc7fb

Please sign in to comment.