feat(linter/eslint): Implement max-classes-per-file (#3241)

Rule Detail:
[link](https://eslint.org/docs/latest/rules/max-classes-per-file)
This commit is contained in:
Jelle van der Waa 2024-05-13 11:23:01 +02:00 committed by GitHub
parent 6149e49ef7
commit 44b16ef79d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 269 additions and 0 deletions

View file

@ -44,6 +44,7 @@ mod eslint {
pub mod for_direction;
pub mod getter_return;
pub mod guard_for_in;
pub mod max_classes_per_file;
pub mod max_lines;
pub mod max_params;
pub mod no_array_constructor;
@ -404,6 +405,7 @@ oxc_macros::declare_all_lint_rules! {
eslint::for_direction,
eslint::getter_return,
eslint::guard_for_in,
eslint::max_classes_per_file,
eslint::max_lines,
eslint::max_params,
eslint::no_ternary,

View file

@ -0,0 +1,196 @@
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_syntax::class::ClassId;
use serde_json::Value;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use crate::{context::LintContext, rule::Rule};
fn max_classes_per_file_diagnostic(total: usize, max: usize, span1: Span) -> OxcDiagnostic {
OxcDiagnostic::warning(format!(
"eslint(max-classes-per-file): File has too many classes ({total}). Maximum allowed is {max}",
))
.with_help("Reduce the number of classes in this file")
.with_labels([span1.into()])
}
#[derive(Debug, Default, Clone)]
pub struct MaxClassesPerFile(Box<MaxClassesPerFileConfig>);
#[derive(Debug, Clone)]
pub struct MaxClassesPerFileConfig {
pub max: usize,
pub ignore_expressions: bool,
}
impl std::ops::Deref for MaxClassesPerFile {
type Target = MaxClassesPerFileConfig;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Default for MaxClassesPerFileConfig {
fn default() -> Self {
Self { max: 1, ignore_expressions: false }
}
}
declare_oxc_lint!(
/// ### What it does
///
/// Enforce a maximum number of classes per file
///
/// ### Why is this bad?
///
/// Files containing multiple classes can often result in a less navigable and poorly
/// structured codebase. Best practice is to keep each file limited to a single responsibility.
///
/// ### Example
/// ```javascript
/// class Foo {}
/// class Bar {}
/// ```
MaxClassesPerFile,
pedantic,
);
impl Rule for MaxClassesPerFile {
fn from_configuration(value: serde_json::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(MaxClassesPerFileConfig { max, ignore_expressions: false }))
} else {
let max = value
.get(0)
.and_then(|config| config.get("max"))
.and_then(serde_json::Value::as_number)
.and_then(serde_json::Number::as_u64)
.map_or(1, |v| usize::try_from(v).unwrap_or(1));
let ignore_expressions = value
.get(0)
.and_then(|config| config.get("ignoreExpressions"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
Self(Box::new(MaxClassesPerFileConfig { max, ignore_expressions }))
}
}
fn run_once(&self, ctx: &LintContext<'_>) {
let mut class_count = ctx.semantic().classes().declarations.len();
if self.ignore_expressions {
let class_expressions = ctx
.semantic()
.classes()
.iter_enumerated()
.filter(|(_class_id, node_id)| !ctx.nodes().kind(**node_id).is_declaration())
.count();
class_count -= class_expressions;
}
if class_count <= self.max {
return;
}
let ast_node_id = ctx.semantic().classes().get_node_id(ClassId::from(self.max));
let span = if let AstKind::Class(class) = ctx.nodes().kind(ast_node_id) {
class.span
} else {
Span::new(0, 0)
};
ctx.diagnostic(max_classes_per_file_diagnostic(class_count, self.max, span));
}
}
#[test]
fn test() {
use crate::tester::Tester;
let pass = vec![
("class Foo {}", None),
("var x = class {};", None),
("var x = 5;", None),
("class Foo {}", Some(serde_json::json!([1]))),
(
"class Foo {}
class Bar {}",
Some(serde_json::json!([2])),
),
("class Foo {}", Some(serde_json::json!([{ "max": 1 }]))),
(
"class Foo {}
class Bar {}",
Some(serde_json::json!([{ "max": 2 }])),
),
(
"
class Foo {}
const myExpression = class {}
",
Some(serde_json::json!([{ "ignoreExpressions": true, "max": 1 }])),
),
(
"
class Foo {}
class Bar {}
const myExpression = class {}
",
Some(serde_json::json!([{ "ignoreExpressions": true, "max": 2 }])),
),
];
let fail = vec![
(
"class Foo {}
class Bar {}",
None,
),
(
"class Foo {}
const myExpression = class {}",
None,
),
(
"var x = class {};
var y = class {};",
None,
),
(
"class Foo {}
var x = class {};",
None,
),
("class Foo {} class Bar {}", Some(serde_json::json!([1]))),
("class Foo {} class Bar {} class Baz {}", Some(serde_json::json!([2]))),
(
"
class Foo {}
class Bar {}
const myExpression = class {}
",
Some(serde_json::json!([{ "ignoreExpressions": true, "max": 1 }])),
),
(
"
class Foo {}
class Bar {}
class Baz {}
const myExpression = class {}
",
Some(serde_json::json!([{ "ignoreExpressions": true, "max": 2 }])),
),
];
Tester::new(MaxClassesPerFile::NAME, pass, fail).test_and_snapshot();
}

View file

@ -0,0 +1,67 @@
---
source: crates/oxc_linter/src/tester.rs
expression: max_classes_per_file
---
⚠ eslint(max-classes-per-file): File has too many classes (2). Maximum allowed is 1
╭─[max_classes_per_file.tsx:2:4]
1 │ class Foo {}
2 │ class Bar {}
· ────────────
╰────
help: Reduce the number of classes in this file
⚠ eslint(max-classes-per-file): File has too many classes (2). Maximum allowed is 1
╭─[max_classes_per_file.tsx:2:25]
1 │ class Foo {}
2 │ const myExpression = class {}
· ────────
╰────
help: Reduce the number of classes in this file
⚠ eslint(max-classes-per-file): File has too many classes (2). Maximum allowed is 1
╭─[max_classes_per_file.tsx:2:12]
1 │ var x = class {};
2 │ var y = class {};
· ────────
╰────
help: Reduce the number of classes in this file
⚠ eslint(max-classes-per-file): File has too many classes (2). Maximum allowed is 1
╭─[max_classes_per_file.tsx:2:12]
1 │ class Foo {}
2 │ var x = class {};
· ────────
╰────
help: Reduce the number of classes in this file
⚠ eslint(max-classes-per-file): File has too many classes (2). Maximum allowed is 1
╭─[max_classes_per_file.tsx:1:14]
1 │ class Foo {} class Bar {}
· ────────────
╰────
help: Reduce the number of classes in this file
⚠ eslint(max-classes-per-file): File has too many classes (3). Maximum allowed is 2
╭─[max_classes_per_file.tsx:1:27]
1 │ class Foo {} class Bar {} class Baz {}
· ────────────
╰────
help: Reduce the number of classes in this file
⚠ eslint(max-classes-per-file): File has too many classes (2). Maximum allowed is 1
╭─[max_classes_per_file.tsx:3:20]
2 │ class Foo {}
3 │ class Bar {}
· ────────────
4 │ const myExpression = class {}
╰────
help: Reduce the number of classes in this file
⚠ eslint(max-classes-per-file): File has too many classes (3). Maximum allowed is 2
╭─[max_classes_per_file.tsx:4:20]
3 │ class Bar {}
4 │ class Baz {}
· ────────────
5 │ const myExpression = class {}
╰────
help: Reduce the number of classes in this file

View file

@ -57,6 +57,10 @@ impl ClassTable {
std::iter::successors(Some(class_id), |class_id| self.parent_ids.get(class_id).copied())
}
pub fn len(&self) -> usize {
self.declarations.raw.len()
}
pub fn iter_enumerated(&self) -> impl Iterator<Item = (ClassId, &AstNodeId)> + '_ {
self.declarations.iter_enumerated()
}