feat(linter/import): Implement max-dependencies (#3814)

Rule Detail:

[link](https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/docs/rules/max-dependencies.md)

---

This lacks the handling of `require()` which seems to be the case for
most existing `import` rules.

Another "issue" could be if you have:
```
import { foo } from "./foo";
import { bar } from "./foo";
```

But then again there should be another rule to filter these duplicate
imports out and combine them into one.

Co-authored-by: Don Isaac <donald.isaac@gmail.com>
This commit is contained in:
Jelle van der Waa 2024-06-25 19:18:01 +02:00 committed by GitHub
parent 2e026e1b7f
commit fafe67c817
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 242 additions and 0 deletions

View file

@ -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,

View file

@ -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)
}
/// <https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/docs/rules/max-dependencies.md>
#[derive(Debug, Default, Clone)]
pub struct MaxDependencies(Box<MaxDependenciesConfig>);
#[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();
}

View file

@ -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