Skip to content

Commit b9be0f8

Browse files
committed
feat(migrate): migrate nested configuration files
1 parent cb1005c commit b9be0f8

28 files changed

+401
-107
lines changed

.changeset/whole-clouds-lead.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": minor
3+
---
4+
5+
The command `migrate` is now able to migrate nested configuration files.

crates/biome_cli/src/commands/mod.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,8 +777,16 @@ pub(crate) trait CommandRunner: Sized {
777777
execution,
778778
paths,
779779
duration,
780+
configuration_files,
780781
} = self.configure_workspace(fs, console, workspace, cli_options)?;
781-
execute_mode(execution, session, cli_options, paths, duration)
782+
execute_mode(
783+
execution,
784+
session,
785+
cli_options,
786+
paths,
787+
duration,
788+
configuration_files,
789+
)
782790
}
783791

784792
/// This function prepares the workspace with the following:
@@ -864,6 +872,7 @@ pub(crate) trait CommandRunner: Sized {
864872
execution,
865873
paths,
866874
duration: Some(result.duration),
875+
configuration_files: result.configuration_files,
867876
})
868877
}
869878

@@ -944,6 +953,8 @@ pub(crate) struct ConfiguredWorkspace {
944953
pub paths: Vec<OsString>,
945954
/// The duration of the scanning
946955
pub duration: Option<Duration>,
956+
/// Configuration files found inside the project
957+
pub configuration_files: Vec<BiomePath>,
947958
}
948959

949960
pub trait LoadEditorConfig: CommandRunner {

crates/biome_cli/src/commands/scan_kind.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,19 @@ use rustc_hash::FxHashSet;
1414
///
1515
/// Rules:
1616
/// - CLI via `stdin` return [ScanKind::None]
17+
/// - `biome migrate` return [ScanKind:KnownFiles], so it can migrate all nested configuration files
1718
/// - `biome format` return [ScanKind::KnownFiles] if VCS is enabled, otherwise [ScanKind::None]
1819
/// - `biome lint`, `biome check` and `biome ci` may vary. It depends on whether the user has enabled rules that require the `RuleDomain::Project`.
1920
/// If not, returns [ScanKind::KnownFiles] if VCS is enabled, otherwise [ScanKind::None]
2021
pub(crate) fn compute_scan_kind(execution: &Execution, configuration: &Configuration) -> ScanKind {
21-
if execution.is_stdin() || execution.is_migrate() {
22+
if execution.is_stdin() {
2223
return ScanKind::None;
2324
};
2425

26+
if execution.is_migrate() {
27+
return ScanKind::KnownFiles;
28+
}
29+
2530
if execution.is_format() {
2631
// There's no need to scan further known files if the VCS isn't enabled
2732
return if !configuration.use_ignore_file() {

crates/biome_cli/src/execute/migrate.rs

Lines changed: 94 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ use crate::execute::diagnostics::{ContentDiffAdvice, MigrateDiffDiagnostic};
44
use crate::{CliDiagnostic, CliSession};
55
use biome_analyze::AnalysisFilter;
66
use biome_configuration::Configuration;
7-
use biome_console::{ConsoleExt, markup};
7+
use biome_console::{Console, ConsoleExt, markup};
88
use biome_deserialize::Merge;
99
use biome_deserialize::json::deserialize_from_json_ast;
10-
use biome_diagnostics::Diagnostic;
1110
use biome_diagnostics::{PrintDiagnostic, category};
1211
use biome_fs::{BiomePath, OpenOptions};
1312
use biome_json_parser::{JsonParserOptions, parse_json_with_cache};
1413
use biome_json_syntax::{JsonFileSource, JsonRoot};
1514
use biome_migrate::{ControlFlow, migrate_configuration};
1615
use biome_rowan::{AstNode, NodeCache};
16+
use biome_service::Workspace;
1717
use biome_service::projects::ProjectKey;
1818
use biome_service::workspace::{
1919
ChangeFileParams, FileContent, FixAction, FormatFileParams, OpenFileParams,
@@ -37,8 +37,8 @@ pub(crate) struct MigratePayload<'a> {
3737
pub(crate) project_key: ProjectKey,
3838
pub(crate) write: bool,
3939
pub(crate) configuration_file_path: Utf8PathBuf,
40-
pub(crate) verbose: bool,
4140
pub(crate) sub_command: Option<MigrateSubCommand>,
41+
pub(crate) nested_configuration_files: Vec<BiomePath>,
4242
}
4343

4444
pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic> {
@@ -47,12 +47,78 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
4747
project_key,
4848
write,
4949
configuration_file_path,
50-
verbose,
5150
sub_command,
51+
nested_configuration_files,
5252
} = migrate_payload;
53-
let mut cache = NodeCache::default();
53+
let workspace = &*session.app.workspace;
5454
let console = session.app.console;
55-
let workspace = session.app.workspace;
55+
56+
let mut configuration_list = vec![configuration_file_path.into()];
57+
configuration_list.extend(nested_configuration_files);
58+
let mut needs_migration = false;
59+
let mut migrated = false;
60+
for configuration_file_path in configuration_list {
61+
let migrate_file_payload = MigrateFile {
62+
workspace,
63+
console,
64+
configuration_file_path,
65+
project_key,
66+
sub_command: sub_command.as_ref(),
67+
write,
68+
};
69+
70+
let result = migrate_file(migrate_file_payload)?;
71+
if let MigrationFileResult::NeedsMigration = result {
72+
needs_migration = true;
73+
} else if let MigrationFileResult::Migrated = result {
74+
migrated = true;
75+
}
76+
}
77+
78+
if needs_migration {
79+
console.log(markup! {
80+
<Info>"Run the command with the option "<Emphasis>"--write"</Emphasis>" to apply the changes."</Info>
81+
})
82+
} else if migrated {
83+
console.log(markup! {
84+
<Info>"Your configuration file(s) have been successfully migrated."</Info>
85+
})
86+
} else {
87+
console.log(markup! {
88+
<Info>"No changes to apply to the Biome configuration file."</Info>
89+
});
90+
}
91+
92+
Ok(())
93+
}
94+
95+
struct MigrateFile<'a> {
96+
pub(crate) workspace: &'a dyn Workspace,
97+
pub(crate) console: &'a mut dyn Console,
98+
pub(crate) project_key: ProjectKey,
99+
pub(crate) write: bool,
100+
pub(crate) configuration_file_path: BiomePath,
101+
pub(crate) sub_command: Option<&'a MigrateSubCommand>,
102+
}
103+
104+
#[derive(Debug, Eq, PartialEq)]
105+
enum MigrationFileResult {
106+
Migrated,
107+
NeedsMigration,
108+
NoMigrationNeeded,
109+
HasErrors,
110+
}
111+
112+
fn migrate_file(payload: MigrateFile) -> Result<MigrationFileResult, CliDiagnostic> {
113+
let MigrateFile {
114+
workspace,
115+
console,
116+
project_key,
117+
write,
118+
configuration_file_path,
119+
sub_command,
120+
} = payload;
121+
let mut cache = NodeCache::default();
56122
let fs = workspace.fs();
57123

58124
let open_options = if write {
@@ -90,7 +156,7 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
90156
let biome_config =
91157
deserialize_from_json_ast::<Configuration>(&parsed.tree(), "").into_deserialized();
92158
let Some(mut biome_config) = biome_config else {
93-
return Ok(());
159+
return Ok(MigrationFileResult::HasErrors);
94160
};
95161
let old_biome_config = biome_config.clone();
96162
let prettier_biome_config = prettier_config.try_into().map_err(|err| {
@@ -118,6 +184,7 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
118184
console.log(markup! {
119185
<Info>"No changes to apply to the Biome configuration file."</Info>
120186
});
187+
Ok(MigrationFileResult::NoMigrationNeeded)
121188
} else {
122189
let new_content = serde_json::to_string(&biome_config).map_err(|err| {
123190
CliDiagnostic::MigrateError(MigrationDiagnostic {
@@ -139,6 +206,7 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
139206
console.log(markup!{
140207
<Info><Emphasis>{prettier_path}</Emphasis>" has been successfully migrated."</Info>
141208
});
209+
Ok(MigrationFileResult::Migrated)
142210
} else {
143211
let file_name = configuration_file_path.to_string();
144212
let diagnostic = MigrateDiffDiagnostic {
@@ -149,9 +217,7 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
149217
},
150218
};
151219
console.error(markup! {{PrintDiagnostic::simple(&diagnostic)}});
152-
console.log(markup! {
153-
<Info>"Run the command with the option "<Emphasis>"--write"</Emphasis>" to apply the changes."</Info>
154-
})
220+
Ok(MigrationFileResult::NeedsMigration)
155221
}
156222
}
157223
}
@@ -166,12 +232,12 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
166232
let biome_config =
167233
deserialize_from_json_ast::<Configuration>(&parsed.tree(), "").into_deserialized();
168234
let Some(mut biome_config) = biome_config else {
169-
return Ok(());
235+
return Ok(MigrationFileResult::HasErrors);
170236
};
171237
let (biome_eslint_config, results) =
172238
eslint_config.into_biome_config(&eslint_to_biome::MigrationOptions {
173-
include_inspired,
174-
include_nursery,
239+
include_inspired: *include_inspired,
240+
include_nursery: *include_nursery,
175241
});
176242
let old_biome_config = biome_config.clone();
177243
biome_config.merge_with(biome_eslint_config);
@@ -190,10 +256,11 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
190256
});
191257
}
192258
}
193-
if biome_config == old_biome_config {
259+
let result = if biome_config == old_biome_config {
194260
console.log(markup! {
195261
<Info>"No changes to apply to the Biome configuration file."</Info>
196262
});
263+
MigrationFileResult::NoMigrationNeeded
197264
} else {
198265
let new_content = serde_json::to_string(&biome_config).map_err(|err| {
199266
CliDiagnostic::MigrateError(MigrationDiagnostic {
@@ -215,6 +282,7 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
215282
console.log(markup!{
216283
<Info><Emphasis>{eslint_path}</Emphasis>" has been successfully migrated."</Info>
217284
});
285+
MigrationFileResult::Migrated
218286
} else {
219287
let file_name = configuration_file_path.to_string();
220288
let diagnostic = MigrateDiffDiagnostic {
@@ -225,19 +293,17 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
225293
},
226294
};
227295
console.error(markup! {{PrintDiagnostic::simple(&diagnostic)}});
228-
console.log(markup! {
229-
<Info>"Run the command with the option "<Emphasis>"--write"</Emphasis>" to apply the changes."</Info>
230-
})
296+
MigrationFileResult::NeedsMigration
231297
}
232-
}
298+
};
233299
if results.has_inspired_rules {
234300
console.log(markup! {
235301
<Info>"Run the command with the option "<Emphasis>"--include-inspired"</Emphasis>" to also migrate inspired rules."</Info>
236-
})
302+
});
237303
}
304+
Ok(result)
238305
}
239306
None => {
240-
let mut errors = 0;
241307
let mut tree = parsed.tree();
242308
let mut actions = Vec::new();
243309
loop {
@@ -246,10 +312,6 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
246312
AnalysisFilter::default(),
247313
configuration_file_path.as_path(),
248314
|signal| {
249-
let current_diagnostic = signal.diagnostic();
250-
if current_diagnostic.is_some() {
251-
errors += 1;
252-
}
253315
if let Some(action) = signal.actions().next() {
254316
return ControlFlow::Break(action);
255317
}
@@ -296,9 +358,10 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
296358
path: biome_path,
297359
})?;
298360
configuration_file.set_content(printed.as_code().as_bytes())?;
299-
console.log(markup!{
300-
<Info>"The configuration "<Emphasis>{{configuration_file_path.to_string()}}</Emphasis>" has been successfully migrated."</Info>
301-
})
361+
// console.log(markup!{
362+
// <Info>"The configuration "<Emphasis>{{configuration_file_path.to_string()}}</Emphasis>" has been successfully migrated."</Info>
363+
// })
364+
Ok(MigrationFileResult::Migrated)
302365
} else {
303366
let file_name = configuration_file_path.to_string();
304367
let diagnostic = MigrateDiffDiagnostic {
@@ -308,25 +371,17 @@ pub(crate) fn run(migrate_payload: MigratePayload) -> Result<(), CliDiagnostic>
308371
new: new_configuration_content,
309372
},
310373
};
311-
if diagnostic.tags().is_verbose() {
312-
if verbose {
313-
console.error(markup! {{PrintDiagnostic::verbose(&diagnostic)}})
314-
}
315-
} else {
316-
console.error(markup! {{PrintDiagnostic::simple(&diagnostic)}})
317-
}
318-
console.log(markup! {
319-
<Info>"Run the command with the option "<Emphasis>"--write"</Emphasis>" to apply the changes."</Info>
320-
})
374+
console.error(markup! {{PrintDiagnostic::simple(&diagnostic)}});
375+
Ok(MigrationFileResult::NeedsMigration)
321376
}
322377
} else {
323378
console.log(markup! {
324379
<Info>
325380
"Your configuration file is up to date."
326381
</Info>
327-
})
382+
});
383+
Ok(MigrationFileResult::NoMigrationNeeded)
328384
}
329385
}
330386
}
331-
Ok(())
332387
}

crates/biome_cli/src/execute/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ pub fn execute_mode(
562562
cli_options: &CliOptions,
563563
paths: Vec<OsString>,
564564
scanner_duration: Option<Duration>,
565+
nested_configuration_files: Vec<BiomePath>,
565566
) -> Result<(), CliDiagnostic> {
566567
// If a custom reporter was provided, let's lift the limit so users can see all of them
567568
execution.max_diagnostics = if cli_options.reporter.is_default() {
@@ -587,8 +588,8 @@ pub fn execute_mode(
587588
project_key,
588589
write,
589590
configuration_file_path,
590-
verbose: cli_options.verbose,
591591
sub_command,
592+
nested_configuration_files,
592593
};
593594
return migrate::run(payload);
594595
}

crates/biome_cli/tests/commands/migrate.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use crate::run_cli;
21
use crate::snap_test::{SnapshotPayload, assert_cli_snapshot, assert_file_contents};
2+
use crate::{run_cli, run_cli_with_dyn_fs};
33
use biome_console::BufferConsole;
4-
use biome_fs::MemoryFileSystem;
4+
use biome_fs::{MemoryFileSystem, TemporaryFs};
55
use bpaf::Args;
66
use camino::Utf8Path;
77

@@ -95,3 +95,33 @@ fn should_emit_incompatible_arguments_error() {
9595
result,
9696
));
9797
}
98+
99+
#[test]
100+
fn should_migrate_nested_files() {
101+
let mut fs = TemporaryFs::new("missing_configuration_file");
102+
let mut console = BufferConsole::default();
103+
104+
let configuration = r#"{
105+
"organizeImports": {
106+
"enabled": true
107+
}
108+
}"#;
109+
fs.create_file("biome.json", configuration);
110+
fs.create_file("lorem/biome.json", configuration);
111+
fs.create_file("ipsum/biome.json", configuration);
112+
113+
let result = run_cli_with_dyn_fs(
114+
Box::new(fs.create_os()),
115+
&mut console,
116+
Args::from(["migrate"].as_slice()),
117+
);
118+
assert!(result.is_ok(), "run_cli returned {result:?}");
119+
120+
assert_cli_snapshot(SnapshotPayload::new(
121+
module_path!(),
122+
"should_migrate_nested_files",
123+
fs.create_mem(),
124+
console,
125+
result,
126+
));
127+
}

0 commit comments

Comments
 (0)