diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index b89f807fb1514..a6d7156367a4c 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -15,6 +15,7 @@ mod import { pub mod no_cycle; pub mod no_default_export; // pub mod no_deprecated; + pub mod max_dependencies; pub mod no_duplicates; pub mod no_named_as_default; pub mod no_named_as_default_member; @@ -694,6 +695,7 @@ oxc_macros::declare_all_lint_rules! { react_perf::jsx_no_new_object_as_prop, import::default, import::export, + import::max_dependencies, import::named, import::namespace, import::no_amd, diff --git a/crates/oxc_linter/src/rules/import/max_dependencies.rs b/crates/oxc_linter/src/rules/import/max_dependencies.rs new file mode 100644 index 0000000000000..76b608cf9e06f --- /dev/null +++ b/crates/oxc_linter/src/rules/import/max_dependencies.rs @@ -0,0 +1,202 @@ +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use serde_json::Value; + +use crate::{context::LintContext, rule::Rule}; + +fn max_dependencies_diagnostic(x0: &str, span1: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("eslint-plugin-import(max-dependencies): {x0:?}")) + .with_help("Reduce the number of dependencies in this file") + .with_label(span1) +} + +/// +#[derive(Debug, Default, Clone)] +pub struct MaxDependencies(Box); + +#[derive(Debug, Clone)] +pub struct MaxDependenciesConfig { + max: usize, + ignore_type_imports: bool, +} + +impl std::ops::Deref for MaxDependencies { + type Target = MaxDependenciesConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Default for MaxDependenciesConfig { + fn default() -> Self { + Self { max: 10, ignore_type_imports: false } + } +} + +declare_oxc_lint!( + /// ### What it does + /// + /// Forbid modules to have too many dependencies (import or require statements). + /// + /// ### Why is this bad? + /// + /// This is a useful rule because a module with too many dependencies is a code smell, and + /// usually indicates the module is doing too much and/or should be broken up into smaller + /// modules. + /// + /// ### Example + /// + /// Given `{"max": 2}` + /// ```javascript + /// import a from './a'; + /// import b from './b'; + /// import c from './c'; + /// ``` + + MaxDependencies, + pedantic, +); + +impl Rule for MaxDependencies { + fn from_configuration(value: Value) -> Self { + let config = value.get(0); + if let Some(max) = config + .and_then(Value::as_number) + .and_then(serde_json::Number::as_u64) + .and_then(|v| usize::try_from(v).ok()) + { + Self(Box::new(MaxDependenciesConfig { max, ignore_type_imports: false })) + } else { + let max = config + .and_then(|config| config.get("max")) + .and_then(Value::as_number) + .and_then(serde_json::Number::as_u64) + .map_or(10, |v| usize::try_from(v).unwrap_or(10)); + let ignore_type_imports = config + .and_then(|config| config.get("ignoreTypeImports")) + .and_then(Value::as_bool) + .unwrap_or(false); + + Self(Box::new(MaxDependenciesConfig { max, ignore_type_imports })) + } + } + + fn run_once(&self, ctx: &LintContext<'_>) { + let module_record = ctx.module_record(); + let mut module_count = module_record.import_entries.len(); + + let Some(entry) = module_record.import_entries.get(self.max) else { + return; + }; + + if self.ignore_type_imports { + let type_imports = + module_record.import_entries.iter().filter(|entry| entry.is_type).count(); + + module_count -= type_imports; + } + + if module_count <= self.max { + return; + } + + let error = format!( + "File has too many dependencies ({}). Maximum allowed is {}.", + module_count, self.max, + ); + ctx.diagnostic(max_dependencies_diagnostic(&error, entry.module_request.span())); + } +} + +#[test] +fn test() { + use serde_json::json; + + use crate::tester::Tester; + + let pass = vec![ + ( + r" + import './foo.js'; + ", + None, + ), + ( + r" + import './foo.js'; + import './bar.js';", + None, + ), + // ( + // r" + // import './foo.js'; + // import './bar.js'; + // const a = require('./foo.js'); + // const b = require('./bar.js'); + // ", + // Some(json!([{"max": 2}])), + // ), + ( + r" + import {x, y, z} from './foo'; + ", + None, + ), + ( + r" + import type { x } from './foo'; + import type { y } from './foo'; + ", + Some(json!([{"max": 1, "ignoreTypeImports": true}])), + ), + ]; + + let fail = vec![ + ( + r" + import { x } from './foo'; + import { y } from './foo'; + import { z } from './bar'; + ", + Some(json!([{"max": 1}])), + ), + ( + r" + import { x } from './foo'; + import { y } from './foo'; + import { z } from './baz'; + ", + Some(json!([{"max": 2}])), + ), + // ( + // r" + // import { x } from './foo'; + // require('./bar'); const path = require('path'); + // import { z } from './baz'; + // ", + // Some(json!([{"max": 2}])), + // ), + ( + r" + import type { x } from './foo'; + import type { y } from './foo'; + ", + Some(json!([{"max": 1, }])), + ), + ( + r" + import type { x } from './foo'; + import type { y } from './foo'; + import type { z } from './baz'; + ", + Some(json!([{"max": 2, "ignoreTypeImports": false}])), + ), + ]; + + Tester::new(MaxDependencies::NAME, pass, fail) + .change_rule_path("index.ts") + .with_import_plugin(true) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/max_dependencies.snap b/crates/oxc_linter/src/snapshots/max_dependencies.snap new file mode 100644 index 0000000000000..952f19748b5fa --- /dev/null +++ b/crates/oxc_linter/src/snapshots/max_dependencies.snap @@ -0,0 +1,38 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-import(max-dependencies): "File has too many dependencies (3). Maximum allowed is 1." + ╭─[index.ts:3:31] + 2 │ import { x } from './foo'; + 3 │ import { y } from './foo'; + · ─────── + 4 │ import { z } from './bar'; + ╰──── + help: Reduce the number of dependencies in this file + + ⚠ eslint-plugin-import(max-dependencies): "File has too many dependencies (3). Maximum allowed is 2." + ╭─[index.ts:4:31] + 3 │ import { y } from './foo'; + 4 │ import { z } from './baz'; + · ─────── + 5 │ + ╰──── + help: Reduce the number of dependencies in this file + + ⚠ eslint-plugin-import(max-dependencies): "File has too many dependencies (2). Maximum allowed is 1." + ╭─[index.ts:3:36] + 2 │ import type { x } from './foo'; + 3 │ import type { y } from './foo'; + · ─────── + 4 │ + ╰──── + help: Reduce the number of dependencies in this file + + ⚠ eslint-plugin-import(max-dependencies): "File has too many dependencies (3). Maximum allowed is 2." + ╭─[index.ts:4:36] + 3 │ import type { y } from './foo'; + 4 │ import type { z } from './baz'; + · ─────── + 5 │ + ╰──── + help: Reduce the number of dependencies in this file