Skip to content

Commit ae659dd

Browse files
authored
feat(lint/js): add noExcessiveNestedCallbacks (#10188)
1 parent aa5ccb8 commit ae659dd

29 files changed

Lines changed: 1238 additions & 10 deletions

File tree

.changeset/thick-animals-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added a new nursery rule [`noExcessiveNestedCallbacks`](https://biomejs.dev/linter/rules/no-excessive-nested-callbacks/), which disallows callbacks nested deeper than the configured maximum.

.claude/skills/eslint-migrate-options/SKILL.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,17 +290,16 @@ At minimum, verify all of these:
290290
2. ESLint config with options produces the expected Biome options.
291291
3. Unsupported ESLint knobs do not break deserialization.
292292
4. Empty or partially specified nested options do not emit incorrect Biome config.
293+
5. If Biome's defaults differ from ESLint's, severity-only configs should not emit Biome options that change behavior.
293294

294-
Use the migrator spec fixtures in `crates/biome_cli/tests/specs/migrate_eslint/` as the default test path for custom migrators.
295+
Use the migrator spec fixtures in `crates/biome_cli/tests/specs/migrate_eslint/` for custom migrators.
295296

296297
- Add one fixture file per case.
297298
- Keep the fixture focused on `eslint` input and pre-migration `biome` config input.
298299
- Let the generated test runner in `eslint_to_biome.rs` discover the file and write the adjacent `.snap.new`.
299-
- Prefer adding or updating these fixture snapshots instead of writing a new full CLI test when you are verifying custom option migration behavior.
300+
- Add fixtures for every relevant option shape, including severity-only configs when defaults differ between ESLint and Biome.
300301
- After inspecting snapshot differences, use `cargo insta accept` to accept valid new snapshots, or `cargo insta reject` to reject invalid ones and keep iterating.
301302

302-
CLI tests in `crates/biome_cli/tests/commands/migrate_eslint.rs` should be treated as smoke coverage for command wiring and end-to-end behavior, not the primary place to test custom migrators.
303-
304303
Useful commands:
305304

306305
```shell

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_cli/src/execute/migrate/eslint_eslint.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,11 @@ impl Deserializable for Rules {
535535
};
536536
match rule_name.text() {
537537
// Eslint rules with options that we handle
538+
"max-nested-callbacks" => {
539+
if let Some(conf) = RuleConf::deserialize(ctx, &value, name) {
540+
result.insert(Rule::MaxNestedCallbacks(conf));
541+
}
542+
}
538543
"no-console" => {
539544
if let Some(conf) = RuleConf::deserialize(ctx, &value, name) {
540545
result.insert(Rule::NoConsole(conf));
@@ -617,6 +622,59 @@ impl From<NoConsoleOptions> for biome_rule_options::no_console::NoConsoleOptions
617622
}
618623
}
619624

625+
#[derive(Debug, Default)]
626+
pub(crate) struct MaxNestedCallbacksOptions {
627+
max: Option<u8>,
628+
}
629+
630+
impl MaxNestedCallbacksOptions {
631+
const ESLINT_DEFAULT_MAX: u8 = 10;
632+
}
633+
634+
impl Deserializable for MaxNestedCallbacksOptions {
635+
fn deserialize(
636+
ctx: &mut impl DeserializationContext,
637+
value: &impl DeserializableValue,
638+
name: &str,
639+
) -> Option<Self> {
640+
if value.visitable_type()? == DeserializableType::Number {
641+
return Some(Self {
642+
max: Deserializable::deserialize(ctx, value, name),
643+
});
644+
}
645+
646+
MaxNestedCallbacksObjectOptions::deserialize(ctx, value, name).map(Into::into)
647+
}
648+
}
649+
650+
#[derive(Debug, Default, Deserializable)]
651+
pub(crate) struct MaxNestedCallbacksObjectOptions {
652+
max: Option<u8>,
653+
maximum: Option<u8>,
654+
}
655+
656+
impl From<MaxNestedCallbacksObjectOptions> for MaxNestedCallbacksOptions {
657+
fn from(value: MaxNestedCallbacksObjectOptions) -> Self {
658+
Self {
659+
max: value.max.or(value.maximum),
660+
}
661+
}
662+
}
663+
664+
impl From<MaxNestedCallbacksOptions>
665+
for biome_rule_options::no_excessive_nested_callbacks::NoExcessiveNestedCallbacksOptions
666+
{
667+
fn from(value: MaxNestedCallbacksOptions) -> Self {
668+
Self {
669+
max: Some(
670+
value
671+
.max
672+
.unwrap_or(MaxNestedCallbacksOptions::ESLINT_DEFAULT_MAX),
673+
),
674+
}
675+
}
676+
}
677+
620678
#[derive(Debug)]
621679
pub(crate) enum NoRestrictedGlobal {
622680
Plain(String),
@@ -662,6 +720,7 @@ pub(crate) enum Rule {
662720
Any(Cow<'static, str>, Severity),
663721
// Eslint rules with its options
664722
// We use this to configure equivalent Bione's rules.
723+
MaxNestedCallbacks(RuleConf<MaxNestedCallbacksOptions>),
665724
NoConsole(RuleConf<Box<NoConsoleOptions>>),
666725
NoRestrictedGlobals(RuleConf<Box<NoRestrictedGlobal>>),
667726
// Eslint plugins
@@ -681,6 +740,7 @@ impl Rule {
681740
pub(crate) fn name(&self) -> Cow<'static, str> {
682741
match self {
683742
Self::Any(name, _) => name.clone(),
743+
Self::MaxNestedCallbacks(_) => Cow::Borrowed("max-nested-callbacks"),
684744
Self::NoConsole(_) => Cow::Borrowed("no-console"),
685745
Self::NoRestrictedGlobals(_) => Cow::Borrowed("no-restricted-globals"),
686746
Self::JestConsistentTestIt(_) => Cow::Borrowed("jest/consistent-test-it"),

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,20 @@ fn migrate_eslint_rule(
644644
eslint_eslint::Rule::Any(name, severity) => {
645645
let _ = migrate_eslint_any_rule(rules, &name, severity, opts, results);
646646
}
647+
eslint_eslint::Rule::MaxNestedCallbacks(conf) => {
648+
if migrate_eslint_any_rule(rules, &name, conf.severity(), opts, results) {
649+
let group = rules.nursery.get_or_insert_with(Default::default);
650+
if let SeverityOrGroup::Group(group) = group {
651+
group.no_excessive_nested_callbacks =
652+
Some(biome_config::RuleConfiguration::WithOptions(
653+
biome_config::RuleWithOptions {
654+
level: conf.severity().into(),
655+
options: conf.option_or_default().into(),
656+
},
657+
));
658+
}
659+
}
660+
}
647661
eslint_eslint::Rule::NoConsole(conf) => {
648662
if migrate_eslint_any_rule(rules, &name, conf.severity(), opts, results)
649663
&& let eslint_eslint::RuleConf::Option(severity, rule_options) = conf
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"biome": {},
3+
"eslint": {
4+
"rules": {
5+
"max-nested-callbacks": ["warn", { "maximum": 5 }]
6+
}
7+
}
8+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
source: crates/biome_cli/src/execute/migrate/eslint_to_biome.rs
3+
expression: deprecated_maximum.jsonc
4+
---
5+
## ESLint Config
6+
7+
```json
8+
{ "rules": { "max-nested-callbacks": ["warn", { "maximum": 5 }] } }
9+
10+
```
11+
12+
## Biome Config Before Migration
13+
14+
```json
15+
{}
16+
17+
```
18+
19+
## Biome Config After Migration
20+
21+
```json
22+
{
23+
"linter": {
24+
"rules": {
25+
"recommended": false,
26+
"nursery": {
27+
"noExcessiveNestedCallbacks": {
28+
"level": "warn",
29+
"options": { "max": 5 }
30+
}
31+
}
32+
}
33+
}
34+
}
35+
36+
```
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"biome": {},
3+
"eslint": {
4+
"rules": {
5+
"max-nested-callbacks": ["error", 4]
6+
}
7+
}
8+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
source: crates/biome_cli/src/execute/migrate/eslint_to_biome.rs
3+
expression: numeric_option.jsonc
4+
---
5+
## ESLint Config
6+
7+
```json
8+
{ "rules": { "max-nested-callbacks": ["error", 4] } }
9+
10+
```
11+
12+
## Biome Config Before Migration
13+
14+
```json
15+
{}
16+
17+
```
18+
19+
## Biome Config After Migration
20+
21+
```json
22+
{
23+
"linter": {
24+
"rules": {
25+
"recommended": false,
26+
"nursery": {
27+
"noExcessiveNestedCallbacks": {
28+
"level": "error",
29+
"options": { "max": 4 }
30+
}
31+
}
32+
}
33+
}
34+
}
35+
36+
```
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"biome": {},
3+
"eslint": {
4+
"rules": {
5+
"max-nested-callbacks": "error"
6+
}
7+
}
8+
}

0 commit comments

Comments
 (0)