Skip to content

Commit

Permalink
feat(linter): add import/no-namespace rule (#7229)
Browse files Browse the repository at this point in the history
  • Loading branch information
pumano authored and Dunqing committed Nov 17, 2024
1 parent bfb7766 commit 0761989
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ dashmap = "6.1.0"
encoding_rs = "0.8.34"
encoding_rs_io = "0.1.7"
env_logger = { version = "0.11.5", default-features = false }
fast-glob = "0.4.0"
flate2 = "1.0.34"
futures = "0.3.31"
glob = "0.3.1"
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_linter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ bitflags = { workspace = true }
convert_case = { workspace = true }
cow-utils = { workspace = true }
dashmap = { workspace = true }
fast-glob = { workspace = true }
globset = { workspace = true, features = ["serde1"] }
itertools = { workspace = true }
json-strip-comments = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod import {
pub mod default;
pub mod export;
pub mod first;
pub mod import_no_namespace;
pub mod max_dependencies;
pub mod named;
pub mod namespace;
Expand Down Expand Up @@ -627,6 +628,7 @@ oxc_macros::declare_all_lint_rules! {
import::default,
import::export,
import::first,
import::import_no_namespace,
import::max_dependencies,
import::named,
import::namespace,
Expand Down
166 changes: 166 additions & 0 deletions crates/oxc_linter/src/rules/import/import_no_namespace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use fast_glob::glob_match;
use oxc_diagnostics::OxcDiagnostic;

use oxc_macros::declare_oxc_lint;
use oxc_span::{CompactStr, Span};
use oxc_syntax::module_record::ImportImportName;

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

fn import_no_namespace_diagnostic(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Usage of namespaced aka wildcard \"*\" imports prohibited")
.with_help("Use named or default imports")
.with_label(span)
}

#[derive(Debug, Default, Clone)]
pub struct ImportNoNamespace(Box<ImportNoNamespaceConfig>);

#[derive(Debug, Default, Clone)]
pub struct ImportNoNamespaceConfig {
ignore: Vec<CompactStr>,
}

impl std::ops::Deref for ImportNoNamespace {
type Target = ImportNoNamespaceConfig;

fn deref(&self) -> &Self::Target {
&self.0
}
}

declare_oxc_lint!(
/// ### What it does
///
/// Enforce a convention of not using namespaced (a.k.a. "wildcard" *) imports.
///
/// ### Why is this bad?
///
/// Namespaced imports, while sometimes used, are generally considered less ideal in modern JavaScript development for several reasons:
///
/// 1. **Specificity and Namespace Pollution**:
/// * **Specificity**: Namespaced imports import the entire module, bringing in everything, even if you only need a few specific functions or classes. This can lead to potential naming conflicts if different modules have the same names for different functions.
/// * **Pollution**: Importing an entire namespace pollutes your current scope with potentially unnecessary functions and variables. It increases the chance of accidental use of an unintended function or variable, leading to harder-to-debug errors.
///
/// 2. **Maintainability**:
/// * **Clarity**: Namespaced imports can make it harder to understand which specific functions or classes are being used in your code. This is especially true in larger projects with numerous imports.
/// * **Refactoring**: If a function or class name changes within the imported module, you might need to update several parts of your code if you are using namespaced imports. This becomes even more challenging when dealing with multiple namespaces.
///
/// 3. **Modern Practice**:
/// * **Explicit Imports**: Modern JavaScript practices encourage explicit imports for specific components. This enhances code readability and maintainability.
/// * **Tree-Shaking**: Tools like Webpack and Rollup use tree-shaking to remove unused code from your bundles. Namespaced imports can prevent efficient tree-shaking, leading to larger bundle sizes.
///
/// ### Options
///
/// `ignore` : array of glob strings for modules that should be ignored by the rule.
///
/// ```json
/// {
/// "ignores": ["*.json"]
/// }
/// ```
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```js
/// import * as user from 'user-lib';
///
/// import some, * as user from './user';
/// ```
///
/// Examples of **correct** code for this rule:
/// ```js
/// import { getUserName, isUser } from 'user-lib';
///
/// import user from 'user-lib';
/// import defaultExport, { isUser } from './user';
/// ```
///
ImportNoNamespace,
style,
pending // TODO: fixer
);

/// <https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-namespace.md>
impl Rule for ImportNoNamespace {
fn from_configuration(value: serde_json::Value) -> Self {
let obj = value.get(0);
Self(Box::new(ImportNoNamespaceConfig {
ignore: obj
.and_then(|v| v.get("ignore"))
.and_then(serde_json::Value::as_array)
.map(|v| {
v.iter().filter_map(serde_json::Value::as_str).map(CompactStr::from).collect()
})
.unwrap_or_default(),
}))
}

fn run_once(&self, ctx: &LintContext<'_>) {
let module_record = ctx.module_record();

if module_record.not_esm {
return;
}

module_record.import_entries.iter().for_each(|entry| {
match &entry.import_name {
ImportImportName::NamespaceObject => {
let source = entry.module_request.name();

if self.ignore.is_empty() {
ctx.diagnostic(import_no_namespace_diagnostic(entry.local_name.span()));
} else {
if !source.contains('.') {
return;
}

if self
.ignore
.iter()
.any(|pattern| glob_match(pattern, source.trim_start_matches("./")))
{
return;
}

ctx.diagnostic(import_no_namespace_diagnostic(entry.local_name.span()));
}
}
ImportImportName::Name(_) | ImportImportName::Default(_) => {}
};
});
}
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
(r"import { a, b } from 'foo';", None),
(r"import { a, b } from './foo';", None),
(r"import bar from 'bar';", None),
(r"import bar from './bar';", None),
(
r"import * as bar from './ignored-module.ext';",
Some(serde_json::json!([{ "ignore": ["*.ext"] }])),
),
(
r"import * as bar from './ignored-module.js';
import * as baz from './other-module.ts'",
Some(serde_json::json!([{ "ignore": ["*.js", "*.ts"] }])),
),
];

let fail = vec![
(r"import * as foo from 'foo';", None),
(r"import defaultExport, * as foo from 'foo';", None),
(r"import * as foo from './foo';", None),
];

Tester::new(ImportNoNamespace::NAME, pass, fail)
.change_rule_path("index.js")
.with_import_plugin(true)
.test_and_snapshot();
}
23 changes: 23 additions & 0 deletions crates/oxc_linter/src/snapshots/import_no_namespace.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
source: crates/oxc_linter/src/tester.rs
---
eslint-plugin-import(import-no-namespace): Usage of namespaced aka wildcard "*" imports prohibited
╭─[index.js:1:13]
1import * as foo from 'foo';
· ───
╰────
help: Use named or default imports

eslint-plugin-import(import-no-namespace): Usage of namespaced aka wildcard "*" imports prohibited
╭─[index.js:1:28]
1import defaultExport, * as foo from 'foo';
· ───
╰────
help: Use named or default imports

eslint-plugin-import(import-no-namespace): Usage of namespaced aka wildcard "*" imports prohibited
╭─[index.js:1:13]
1import * as foo from './foo';
· ───
╰────
help: Use named or default imports

0 comments on commit 0761989

Please sign in to comment.