Skip to content

Commit 71b5c6e

Browse files
authored
feat(js_analyze): implement noExcessiveClassesPerFile (#8753)
1 parent d6b2bda commit 71b5c6e

29 files changed

Lines changed: 488 additions & 1 deletion

.changeset/wide-bananas-boil.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`noExcessiveClassesPerFile`](https://biomejs.dev/linter/rules/no-excessive-classes-per-file). Enforce a maximum number of classes per file.
6+
7+
**Invalid:**
8+
9+
```js
10+
class Foo { }
11+
class Bar { }
12+
```

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod no_duplicate_enum_values;
1212
pub mod no_duplicated_spread_props;
1313
pub mod no_empty_source;
1414
pub mod no_equals_to_null;
15+
pub mod no_excessive_classes_per_file;
1516
pub mod no_excessive_lines_per_file;
1617
pub mod no_floating_classes;
1718
pub mod no_floating_promises;
@@ -64,4 +65,4 @@ pub mod use_spread;
6465
pub mod use_vue_consistent_define_props_declaration;
6566
pub mod use_vue_define_macros_order;
6667
pub mod use_vue_multi_word_component_names;
67-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_div_regex :: NoDivRegex , self :: no_duplicate_enum_values :: NoDuplicateEnumValues , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_floating_classes :: NoFloatingClasses , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_options_api :: NoVueOptionsApi , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_error_cause :: UseErrorCause , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_inline_script_id :: UseInlineScriptId , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
68+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_div_regex :: NoDivRegex , self :: no_duplicate_enum_values :: NoDuplicateEnumValues , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_excessive_classes_per_file :: NoExcessiveClassesPerFile , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_floating_classes :: NoFloatingClasses , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_options_api :: NoVueOptionsApi , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_error_cause :: UseErrorCause , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_inline_script_id :: UseInlineScriptId , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use biome_analyze::{
2+
Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_js_syntax::{AnyJsRoot, JsSyntaxKind};
6+
use biome_rowan::AstNode;
7+
use biome_rule_options::no_excessive_classes_per_file::NoExcessiveClassesPerFileOptions;
8+
9+
declare_lint_rule! {
10+
/// Enforce a maximum number of classes per file.
11+
///
12+
/// Files containing multiple classes can often result in a less navigable and poorly structured codebase.
13+
/// Best practice is to keep each file limited to a single responsibility.
14+
///
15+
/// ## Examples
16+
///
17+
/// ### Invalid
18+
///
19+
/// ```js,expect_diagnostic
20+
/// class Foo {}
21+
/// class Bar {}
22+
/// ```
23+
///
24+
/// ### Valid
25+
///
26+
/// ```js
27+
/// class Foo {}
28+
/// ```
29+
///
30+
/// ## Options
31+
///
32+
/// The following options are available:
33+
///
34+
/// ### `maxClasses`
35+
///
36+
/// This option sets the maximum number of classes allowed in a file.
37+
/// If the file exceeds this limit, a diagnostic will be reported.
38+
///
39+
/// Default: `1`
40+
///
41+
/// ```json,options
42+
/// {
43+
/// "options": {
44+
/// "maxClasses": 2
45+
/// }
46+
/// }
47+
/// ```
48+
///
49+
/// ```js,expect_diagnostic,use_options
50+
/// class Foo {}
51+
/// class Bar {}
52+
/// class Baz {}
53+
/// ```
54+
///
55+
pub NoExcessiveClassesPerFile {
56+
version: "next",
57+
name: "noExcessiveClassesPerFile",
58+
language: "js",
59+
recommended: false,
60+
sources: &[RuleSource::Eslint("max-classes-per-file").same()],
61+
}
62+
}
63+
64+
impl Rule for NoExcessiveClassesPerFile {
65+
type Query = Ast<AnyJsRoot>;
66+
type State = usize;
67+
type Signals = Option<Self::State>;
68+
type Options = NoExcessiveClassesPerFileOptions;
69+
70+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
71+
let node = ctx.query();
72+
let max_classes = ctx.options().max_classes();
73+
74+
let count = node
75+
.syntax()
76+
.descendants()
77+
.filter_map(|n| {
78+
if n.kind() == JsSyntaxKind::JS_CLASS_DECLARATION
79+
|| n.kind() == JsSyntaxKind::JS_CLASS_EXPRESSION
80+
{
81+
Some(())
82+
} else {
83+
None
84+
}
85+
})
86+
.count();
87+
88+
if count > max_classes.get().into() {
89+
return Some(count);
90+
}
91+
92+
None
93+
}
94+
95+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
96+
let node = ctx.query();
97+
let max_classes = ctx.options().max_classes();
98+
Some(
99+
RuleDiagnostic::new(
100+
rule_category!(),
101+
node.range(),
102+
markup! {
103+
"File exceeds the maximum of "{{max_classes.to_string()}}" class"{{if max_classes.get() > 1 { "es" } else { "" }}}", found "{{state}}" classes."
104+
},
105+
)
106+
.note(markup! {
107+
"Files containing multiple classes can often result in a less navigable and poorly structured codebase. Extract the excessive classes into a separate file."
108+
}),
109+
)
110+
}
111+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* should generate diagnostics */
2+
class Outer {
3+
method() {
4+
class Inner { }
5+
}
6+
}
7+
8+
(function() {
9+
return class Foo { };
10+
})();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: edge-cases.js
4+
---
5+
# Input
6+
```js
7+
/* should generate diagnostics */
8+
class Outer {
9+
method() {
10+
class Inner { }
11+
}
12+
}
13+
14+
(function() {
15+
return class Foo { };
16+
})();
17+
18+
```
19+
20+
# Diagnostics
21+
```
22+
edge-cases.js:2:1 lint/nursery/noExcessiveClassesPerFile ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23+
24+
i File exceeds the maximum of 1 class, found 3 classes.
25+
26+
1 │ /* should generate diagnostics */
27+
> 2 │ class Outer {
28+
^^^^^^^^^^^^^
29+
> 3method() {
30+
> 4 │ class Inner { }
31+
...
32+
> 8 │ (function() {
33+
> 9return class Foo { };
34+
> 10 │ })();
35+
^^^^^
36+
11
37+
38+
i Files containing multiple classes can often result in a less navigable and poorly structured codebase. Extract the excessive classes into a separate file.
39+
40+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
41+
42+
43+
```
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/* should generate diagnostics */
2+
class InvalidClass1 { }
3+
class InvalidClass2 { }
4+
class InvalidClass3 { }
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: max-classes-option.js
4+
---
5+
# Input
6+
```js
7+
/* should generate diagnostics */
8+
class InvalidClass1 { }
9+
class InvalidClass2 { }
10+
class InvalidClass3 { }
11+
12+
```
13+
14+
# Diagnostics
15+
```
16+
max-classes-option.js:2:1 lint/nursery/noExcessiveClassesPerFile ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17+
18+
i File exceeds the maximum of 2 classes, found 3 classes.
19+
20+
1 │ /* should generate diagnostics */
21+
> 2 │ class InvalidClass1 { }
22+
│ ^^^^^^^^^^^^^^^^^^^^^^^
23+
> 3 │ class InvalidClass2 { }
24+
> 4 │ class InvalidClass3 { }
25+
│ ^^^^^^^^^^^^^^^^^^^^^^^
26+
5 │
27+
28+
i Files containing multiple classes can often result in a less navigable and poorly structured codebase. Extract the excessive classes into a separate file.
29+
30+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
31+
32+
33+
```

0 commit comments

Comments
 (0)