diff --git a/CHANGELOG.md b/CHANGELOG.md index 7978ef57ee4..3f22cfa317d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ ## [Unreleased] ### CLI + +#### Other changes +- Add new command `rome migrate` the transform the configuration file `rome.json` +when there are breaking changes. + ### Configuration ### Editors ### Formatter diff --git a/Cargo.lock b/Cargo.lock index b5e184928e7..a113a247030 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1669,7 +1669,10 @@ dependencies = [ "rome_js_formatter", "rome_json_formatter", "rome_json_parser", + "rome_json_syntax", "rome_lsp", + "rome_migrate", + "rome_rowan", "rome_service", "rome_text_edit", "rome_text_size", @@ -2039,6 +2042,18 @@ dependencies = [ "quote", ] +[[package]] +name = "rome_migrate" +version = "0.1.0" +dependencies = [ + "lazy_static", + "rome_analyze", + "rome_diagnostics", + "rome_json_parser", + "rome_json_syntax", + "rome_rowan", +] + [[package]] name = "rome_parser" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 161307c8b3d..73294426f8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,4 @@ countme = "3.0.1" tokio = { version = "~1.18.5" } insta = "1.21.2" quote = { version = "1.0.21" } +lazy_static = "1.4.0" diff --git a/crates/rome_cli/Cargo.toml b/crates/rome_cli/Cargo.toml index e371e4a55a8..8130a884ddc 100644 --- a/crates/rome_cli/Cargo.toml +++ b/crates/rome_cli/Cargo.toml @@ -26,7 +26,7 @@ tracing = { workspace = true } tracing-tree = "0.2.2" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } tracing-appender = "0.2" -lazy_static = "1.4.0" +lazy_static = { workspace = true } hdrhistogram = { version = "7.5.0", default-features = false } crossbeam = "0.8.1" rayon = "1.5.1" @@ -36,6 +36,11 @@ tokio = { workspace = true, features = ["io-std", "io-util", "net", "time", "rt" anyhow = "1.0.52" dashmap = { workspace = true } rome_text_size = { path = "../rome_text_size" } +rome_json_parser = { path = "../rome_json_parser" } +rome_json_formatter = { path = "../rome_json_formatter" } +rome_json_syntax = { path = "../rome_json_syntax" } +rome_migrate = { path = "../rome_migrate" } +rome_rowan = { path = "../rome_rowan" } [target.'cfg(unix)'.dependencies] libc = "0.2.127" diff --git a/crates/rome_cli/src/commands/check.rs b/crates/rome_cli/src/commands/check.rs index 9cffd8f9b96..01cfa41205b 100644 --- a/crates/rome_cli/src/commands/check.rs +++ b/crates/rome_cli/src/commands/check.rs @@ -7,7 +7,7 @@ use rome_service::workspace::{FixFileMode, UpdateSettingsParams}; /// Handler for the "check" command of the Rome CLI pub(crate) fn check(mut session: CliSession) -> Result<(), CliDiagnostic> { - let (mut configuration, diagnostics) = load_configuration(&mut session)?.consume(); + let (mut configuration, diagnostics, _) = load_configuration(&mut session)?.consume(); if !diagnostics.is_empty() { let console = &mut session.app.console; console.log(markup!{ diff --git a/crates/rome_cli/src/commands/ci.rs b/crates/rome_cli/src/commands/ci.rs index 775fc63dc78..fcb4d9b5d66 100644 --- a/crates/rome_cli/src/commands/ci.rs +++ b/crates/rome_cli/src/commands/ci.rs @@ -11,7 +11,7 @@ use rome_service::workspace::UpdateSettingsParams; /// Handler for the "ci" command of the Rome CLI pub(crate) fn ci(mut session: CliSession) -> Result<(), CliDiagnostic> { - let (mut configuration, diagnostics) = load_configuration(&mut session)?.consume(); + let (mut configuration, diagnostics, _) = load_configuration(&mut session)?.consume(); if !diagnostics.is_empty() { let console = &mut session.app.console; diff --git a/crates/rome_cli/src/commands/format.rs b/crates/rome_cli/src/commands/format.rs index f69719790a9..f467942d1bb 100644 --- a/crates/rome_cli/src/commands/format.rs +++ b/crates/rome_cli/src/commands/format.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; /// Handler for the "format" command of the Rome CLI pub(crate) fn format(mut session: CliSession) -> Result<(), CliDiagnostic> { - let (mut configuration, diagnostics) = load_configuration(&mut session)?.consume(); + let (mut configuration, diagnostics, _) = load_configuration(&mut session)?.consume(); if !diagnostics.is_empty() { let console = &mut session.app.console; console.log(markup!{ diff --git a/crates/rome_cli/src/commands/help.rs b/crates/rome_cli/src/commands/help.rs index ecaf3be0abb..55149021740 100644 --- a/crates/rome_cli/src/commands/help.rs +++ b/crates/rome_cli/src/commands/help.rs @@ -10,12 +10,13 @@ const MAIN: Markup = markup! { - ""ci"" Run the linter and check the formatting of a set of files - ""format"" Run the formatter on a set of files - ""init"" Bootstraps a new rome project - - ""start"" Start the Rome daemon server process - - ""stop"" Stop the Rome daemon server process + - ""help"" Prints this help message - ""lsp-proxy"" Acts as a server for the Language Server Protocol over stdin/stdout + - ""migrate"" It updates the configuration when there are breaking changes - ""rage"" Prints information for debugging + - ""start"" Start the Rome daemon server process + - ""stop"" Stop the Rome daemon server process - ""version"" Shows the Rome version information and quit - - ""help"" Prints this help message ""OPTIONS:"" ""--colors="" Set the formatting mode for markup: \"off\" prints everything as plain text, \"force\" forces the formatting of markup using ANSI even if the console output is determined to be incompatible @@ -146,6 +147,18 @@ const VERSION_HELP_TEXT: Markup = markup! { rome version" }; +const MIGRATE: Markup = markup! { +"Rome migrate: updates the configuration file to a newer version + +""EXAMPLES:"" + rome migrate + rome migrate --write + +""OPTIONS:"" + ""--write"" It writes the contents to disk +" +}; + pub(crate) fn help(session: CliSession, command: Option<&str>) -> Result<(), CliDiagnostic> { let help_text = match command { Some("help") | None => MAIN, @@ -158,6 +171,7 @@ pub(crate) fn help(session: CliSession, command: Option<&str>) -> Result<(), Cli Some("lsp-proxy") => START_LSP_PROXY, Some("version") => VERSION_HELP_TEXT, Some("rage") => RAGE, + Some("migrate") => MIGRATE, Some(cmd) => return Err(CliDiagnostic::new_unknown_help(cmd)), }; diff --git a/crates/rome_cli/src/commands/migrate.rs b/crates/rome_cli/src/commands/migrate.rs new file mode 100644 index 00000000000..df799dca695 --- /dev/null +++ b/crates/rome_cli/src/commands/migrate.rs @@ -0,0 +1,22 @@ +use crate::configuration::load_configuration; +use crate::diagnostics::MigrationDiagnostic; +use crate::execute::{execute_mode, Execution, TraversalMode}; +use crate::{CliDiagnostic, CliSession}; + +/// Handler for the "check" command of the Rome CLI +pub(crate) fn migrate(mut session: CliSession) -> Result<(), CliDiagnostic> { + let (_, _, path) = load_configuration(&mut session)?.consume(); + if let Some(path) = path { + execute_mode( + Execution::new(TraversalMode::Migrate { + write: session.args.contains("--dry-run"), + configuration_path: path, + }), + session, + ) + } else { + Err(CliDiagnostic::MigrateError(MigrationDiagnostic { + reason: "Rome couldn't find the configuration file".to_string(), + })) + } +} diff --git a/crates/rome_cli/src/commands/mod.rs b/crates/rome_cli/src/commands/mod.rs index aa82c4cd87c..a489cb6edaa 100644 --- a/crates/rome_cli/src/commands/mod.rs +++ b/crates/rome_cli/src/commands/mod.rs @@ -4,5 +4,6 @@ pub(crate) mod daemon; pub(crate) mod format; pub(crate) mod help; pub(crate) mod init; +pub(crate) mod migrate; pub(crate) mod rage; pub(crate) mod version; diff --git a/crates/rome_cli/src/commands/rage.rs b/crates/rome_cli/src/commands/rage.rs index e5a25f94b6a..4bccfb0397b 100644 --- a/crates/rome_cli/src/commands/rage.rs +++ b/crates/rome_cli/src/commands/rage.rs @@ -166,6 +166,7 @@ impl Display for RageConfiguration<'_, '_> { match load_config(self.0, ConfigurationBasePath::default()) { Ok(None) => KeyValuePair("Status", markup!("unset")).fmt(fmt)?, Ok(Some(deserialized)) => { + let (deserialized, _) = deserialized; let (configuration, diagnostics) = deserialized.consume(); let status = if !diagnostics.is_empty() { for diagnostic in diagnostics { diff --git a/crates/rome_cli/src/configuration.rs b/crates/rome_cli/src/configuration.rs index f2f63f84f96..352a68b77c9 100644 --- a/crates/rome_cli/src/configuration.rs +++ b/crates/rome_cli/src/configuration.rs @@ -4,11 +4,38 @@ use crate::{CliDiagnostic, CliSession}; use rome_deserialize::Deserialized; use rome_service::{load_config, Configuration, ConfigurationBasePath}; +#[derive(Default)] +pub struct LoadedConfiguration { + deserialized: Deserialized, + path: Option, +} + +impl LoadedConfiguration { + pub fn consume(self) -> (Configuration, Vec, Option) { + let path = self.path; + let (configuration, diagnostics) = self.deserialized.consume(); + (configuration, diagnostics, path) + } +} + +impl From, PathBuf)>> for LoadedConfiguration { + fn from(value: Option<(Deserialized, PathBuf)>) -> Self { + if let Some((deserialized, path)) = value { + LoadedConfiguration { + deserialized, + path: Some(path), + } + } else { + LoadedConfiguration::default() + } + } +} + /// Load the configuration for this session of the CLI, merging the content of /// the `rome.json` file if it exists on disk with common command line options pub(crate) fn load_configuration( session: &mut CliSession, -) -> Result, CliDiagnostic> { +) -> Result { let config_path: Option = session .args .opt_value_from_str("--config-path") @@ -21,5 +48,5 @@ pub(crate) fn load_configuration( let config = load_config(&session.app.fs, base_path)?; - Ok(config.unwrap_or_default()) + Ok(config.into()) } diff --git a/crates/rome_cli/src/diagnostics.rs b/crates/rome_cli/src/diagnostics.rs index b50c7caebdd..ae0a8a99587 100644 --- a/crates/rome_cli/src/diagnostics.rs +++ b/crates/rome_cli/src/diagnostics.rs @@ -52,6 +52,8 @@ pub enum CliDiagnostic { IncompatibleEndConfiguration(IncompatibleEndConfiguration), /// No files processed during the file system traversal NoFilesWereProcessed(NoFilesWereProcessed), + /// Errors thrown when running the `rome migrate` command + MigrateError(MigrationDiagnostic), } #[derive(Debug, Diagnostic)] @@ -258,6 +260,19 @@ pub struct IncompatibleEndConfiguration { )] pub struct NoFilesWereProcessed; +#[derive(Debug, Diagnostic)] +#[diagnostic( + category = "migrate", + severity = Error, + message( + message("Migration has encountered an error: "{{&self.reason}}), + description = "Migration has encountered an error: {reason}" + ) +)] +pub struct MigrationDiagnostic { + pub reason: String, +} + /// Advices for the [CliDiagnostic] #[derive(Debug, Default)] struct CliAdvice { @@ -422,6 +437,7 @@ impl Diagnostic for CliDiagnostic { CliDiagnostic::IncompatibleEndConfiguration(diagnostic) => diagnostic.category(), CliDiagnostic::NoFilesWereProcessed(diagnostic) => diagnostic.category(), CliDiagnostic::FileCheckApply(diagnostic) => diagnostic.category(), + CliDiagnostic::MigrateError(diagnostic) => diagnostic.category(), } } @@ -442,6 +458,7 @@ impl Diagnostic for CliDiagnostic { CliDiagnostic::IncompatibleEndConfiguration(diagnostic) => diagnostic.tags(), CliDiagnostic::NoFilesWereProcessed(diagnostic) => diagnostic.tags(), CliDiagnostic::FileCheckApply(diagnostic) => diagnostic.tags(), + CliDiagnostic::MigrateError(diagnostic) => diagnostic.tags(), } } @@ -462,6 +479,7 @@ impl Diagnostic for CliDiagnostic { CliDiagnostic::IncompatibleEndConfiguration(diagnostic) => diagnostic.severity(), CliDiagnostic::NoFilesWereProcessed(diagnostic) => diagnostic.severity(), CliDiagnostic::FileCheckApply(diagnostic) => diagnostic.severity(), + CliDiagnostic::MigrateError(diagnostic) => diagnostic.severity(), } } @@ -482,6 +500,7 @@ impl Diagnostic for CliDiagnostic { CliDiagnostic::IncompatibleEndConfiguration(diagnostic) => diagnostic.location(), CliDiagnostic::NoFilesWereProcessed(diagnostic) => diagnostic.location(), CliDiagnostic::FileCheckApply(diagnostic) => diagnostic.location(), + CliDiagnostic::MigrateError(diagnostic) => diagnostic.location(), } } @@ -502,6 +521,7 @@ impl Diagnostic for CliDiagnostic { CliDiagnostic::IncompatibleEndConfiguration(diagnostic) => diagnostic.message(fmt), CliDiagnostic::NoFilesWereProcessed(diagnostic) => diagnostic.message(fmt), CliDiagnostic::FileCheckApply(diagnostic) => diagnostic.message(fmt), + CliDiagnostic::MigrateError(diagnostic) => diagnostic.message(fmt), } } @@ -522,6 +542,7 @@ impl Diagnostic for CliDiagnostic { CliDiagnostic::IncompatibleEndConfiguration(diagnostic) => diagnostic.description(fmt), CliDiagnostic::NoFilesWereProcessed(diagnostic) => diagnostic.description(fmt), CliDiagnostic::FileCheckApply(diagnostic) => diagnostic.description(fmt), + CliDiagnostic::MigrateError(diagnostic) => diagnostic.description(fmt), } } @@ -542,6 +563,7 @@ impl Diagnostic for CliDiagnostic { CliDiagnostic::IncompatibleEndConfiguration(diagnostic) => diagnostic.advices(visitor), CliDiagnostic::NoFilesWereProcessed(diagnostic) => diagnostic.advices(visitor), CliDiagnostic::FileCheckApply(diagnostic) => diagnostic.advices(visitor), + CliDiagnostic::MigrateError(diagnostic) => diagnostic.advices(visitor), } } @@ -566,6 +588,7 @@ impl Diagnostic for CliDiagnostic { } CliDiagnostic::NoFilesWereProcessed(diagnostic) => diagnostic.verbose_advices(visitor), CliDiagnostic::FileCheckApply(diagnostic) => diagnostic.verbose_advices(visitor), + CliDiagnostic::MigrateError(diagnostic) => diagnostic.verbose_advices(visitor), } } @@ -586,6 +609,7 @@ impl Diagnostic for CliDiagnostic { CliDiagnostic::IncompatibleEndConfiguration(diagnostic) => diagnostic.source(), CliDiagnostic::NoFilesWereProcessed(diagnostic) => diagnostic.source(), CliDiagnostic::FileCheckApply(diagnostic) => diagnostic.source(), + CliDiagnostic::MigrateError(diagnostic) => diagnostic.source(), } } } diff --git a/crates/rome_cli/src/execute/diagnostics.rs b/crates/rome_cli/src/execute/diagnostics.rs new file mode 100644 index 00000000000..4221d4cbcb6 --- /dev/null +++ b/crates/rome_cli/src/execute/diagnostics.rs @@ -0,0 +1,149 @@ +use rome_diagnostics::adapters::{IoError, StdError}; +use rome_diagnostics::{Advices, Category, Diagnostic, DiagnosticExt, Error, Severity, Visit}; +use rome_text_edit::TextEdit; +use std::io; + +#[derive(Debug, Diagnostic)] +#[diagnostic( + category = "format", + message = "File content differs from formatting output" +)] +pub(crate) struct CIFormatDiffDiagnostic<'a> { + #[location(resource)] + pub(crate) file_name: &'a str, + #[advice] + pub(crate) diff: ContentDiffAdvice<'a>, +} + +#[derive(Debug, Diagnostic)] +#[diagnostic( + category = "organizeImports", + message = "Import statements differs from the output" +)] +pub(crate) struct CIOrganizeImportsDiffDiagnostic<'a> { + #[location(resource)] + pub(crate) file_name: &'a str, + #[advice] + pub(crate) diff: ContentDiffAdvice<'a>, +} + +#[derive(Debug, Diagnostic)] +#[diagnostic( +category = "format", +severity = Information, +message = "Formatter would have printed the following content:" +)] +pub(crate) struct FormatDiffDiagnostic<'a> { + #[location(resource)] + pub(crate) file_name: &'a str, + #[advice] + pub(crate) diff: ContentDiffAdvice<'a>, +} + +#[derive(Debug, Diagnostic)] +#[diagnostic( + category = "organizeImports", + severity = Information, + message = "Import statements could be sorted:" +)] +pub(crate) struct OrganizeImportsDiffDiagnostic<'a> { + #[location(resource)] + pub(crate) file_name: &'a str, + #[advice] + pub(crate) diff: ContentDiffAdvice<'a>, +} + +#[derive(Debug, Diagnostic)] +#[diagnostic( + category = "migrate", + severity = Information, + message = "Configuration file can be updated." +)] +pub(crate) struct MigrateDiffDiagnostic<'a> { + #[location(resource)] + pub(crate) file_name: &'a str, + #[advice] + pub(crate) diff: ContentDiffAdvice<'a>, +} + +#[derive(Debug)] +pub(crate) struct ContentDiffAdvice<'a> { + pub(crate) old: &'a str, + pub(crate) new: &'a str, +} + +impl Advices for ContentDiffAdvice<'_> { + fn record(&self, visitor: &mut dyn Visit) -> io::Result<()> { + let diff = TextEdit::from_unicode_words(self.old, self.new); + visitor.record_diff(&diff) + } +} + +#[derive(Debug, Diagnostic)] +pub(crate) struct TraversalDiagnostic<'a> { + #[location(resource)] + pub(crate) file_name: Option<&'a str>, + #[severity] + pub(crate) severity: Severity, + #[category] + pub(crate) category: &'static Category, + #[message] + #[description] + pub(crate) message: &'a str, +} + +#[derive(Debug, Diagnostic)] +#[diagnostic(category = "internalError/panic", tags(INTERNAL))] +pub(crate) struct PanicDiagnostic { + #[description] + #[message] + pub(crate) message: String, +} + +#[derive(Debug, Diagnostic)] +#[diagnostic(category = "files/missingHandler", message = "unhandled file type")] +pub(crate) struct UnhandledDiagnostic; + +#[derive(Debug, Diagnostic)] +#[diagnostic(category = "parse", message = "Skipped file with syntax errors")] +pub(crate) struct SkippedDiagnostic; + +/// Extension trait for turning [Display]-able error types into [TraversalError] +pub(crate) trait ResultExt { + type Result; + fn with_file_path_and_code( + self, + file_path: String, + code: &'static Category, + ) -> Result; +} + +impl ResultExt for Result +where + E: std::error::Error + Send + Sync + 'static, +{ + type Result = T; + + fn with_file_path_and_code( + self, + file_path: String, + code: &'static Category, + ) -> Result { + self.map_err(move |err| { + StdError::from(err) + .with_category(code) + .with_file_path(file_path) + }) + } +} + +/// Extension trait for turning [io::Error] into [Error] +pub(crate) trait ResultIoExt: ResultExt { + fn with_file_path(self, file_path: String) -> Result; +} + +impl ResultIoExt for io::Result { + fn with_file_path(self, file_path: String) -> Result { + self.map_err(|error| IoError::from(error).with_file_path(file_path)) + } +} diff --git a/crates/rome_cli/src/execute/migrate.rs b/crates/rome_cli/src/execute/migrate.rs new file mode 100644 index 00000000000..f612dce9c2d --- /dev/null +++ b/crates/rome_cli/src/execute/migrate.rs @@ -0,0 +1,102 @@ +use crate::execute::diagnostics::{ContentDiffAdvice, MigrateDiffDiagnostic}; +use crate::{CliDiagnostic, CliSession}; +use rome_console::{markup, ConsoleExt}; +use rome_diagnostics::PrintDiagnostic; +use rome_fs::OpenOptions; +use rome_json_syntax::JsonRoot; +use rome_migrate::{migrate_configuration, ControlFlow}; +use rome_rowan::AstNode; +use rome_service::workspace::FixAction; +use std::borrow::Cow; +use std::path::PathBuf; + +pub(crate) fn run( + session: CliSession, + write: bool, + configuration_path: PathBuf, + verbose: bool, +) -> Result<(), CliDiagnostic> { + let fs = &*session.app.fs; + let open_options = if write { + OpenOptions::default().write(true) + } else { + OpenOptions::default().read(true) + }; + let mut configuration_file = + fs.open_with_options(configuration_path.as_path(), open_options)?; + let mut configuration_content = String::new(); + configuration_file.read_to_string(&mut configuration_content)?; + let parsed = rome_json_parser::parse_json(&configuration_content); + let mut errors = 0; + let mut tree = parsed.tree(); + let mut actions = Vec::new(); + loop { + let (action, _) = migrate_configuration( + &tree.value().unwrap(), + configuration_path.as_path(), + |signal| { + let current_diagnostic = signal.diagnostic(); + + if current_diagnostic.is_some() { + errors += 1; + } + + if let Some(action) = signal.actions().next() { + return ControlFlow::Break(action); + } + + ControlFlow::Continue(()) + }, + ); + + match action { + Some(action) => { + if let Some((range, _)) = action.mutation.as_text_edits() { + tree = match JsonRoot::cast(action.mutation.commit()) { + Some(tree) => tree, + None => return Err(CliDiagnostic::check_error()), + }; + actions.push(FixAction { + rule_name: action + .rule_name + .map(|(group, rule)| (Cow::Borrowed(group), Cow::Borrowed(rule))), + range, + }); + } + } + None => { + break; + } + } + } + let console = &mut *session.app.console; + let new_configuration_content = tree.to_string(); + + if configuration_content != new_configuration_content { + if write { + configuration_file.set_content(tree.to_string().as_bytes())?; + console.log(markup!{ + "The configuration "{{configuration_path.display().to_string()}}" has been successfully migrated" + }) + } else { + let file_name = configuration_path.display().to_string(); + let diagnostic = MigrateDiffDiagnostic { + file_name: &file_name, + diff: ContentDiffAdvice { + old: configuration_content.as_str(), + new: new_configuration_content.as_str(), + }, + }; + console.error(markup! { + {if verbose { PrintDiagnostic::verbose(&diagnostic) } else { PrintDiagnostic::simple(&diagnostic) }} + }); + } + } else { + console.log(markup! { + + "Your configuration file is up to date." + + }) + } + Ok(()) +} diff --git a/crates/rome_cli/src/execute.rs b/crates/rome_cli/src/execute/mod.rs similarity index 81% rename from crates/rome_cli/src/execute.rs rename to crates/rome_cli/src/execute/mod.rs index fdf67aea5d2..c47f8437d71 100644 --- a/crates/rome_cli/src/execute.rs +++ b/crates/rome_cli/src/execute/mod.rs @@ -1,11 +1,14 @@ -use crate::traversal::traverse; +mod diagnostics; +mod migrate; +mod process_file; +mod std_in; +mod traverse; + +use crate::execute::traverse::traverse; use crate::{CliDiagnostic, CliSession}; -use rome_console::{markup, ConsoleExt}; use rome_diagnostics::MAXIMUM_DISPLAYABLE_DIAGNOSTICS; use rome_fs::RomePath; -use rome_service::workspace::{ - FeatureName, FixFileMode, FormatFileParams, Language, OpenFileParams, SupportsFeatureParams, -}; +use rome_service::workspace::{FeatureName, FixFileMode}; use std::path::PathBuf; /// Useful information during the traversal of files and virtual content @@ -51,6 +54,11 @@ pub(crate) enum TraversalMode { /// 2. The content of the file stdin: Option<(PathBuf, String)>, }, + /// This mode is enabled when running the command `rome migrate` + Migrate { + write: bool, + configuration_path: PathBuf, + }, } /// Tells to the execution of the traversal how the information should be reported @@ -139,6 +147,7 @@ impl Execution { TraversalMode::Check { fix_file_mode } => fix_file_mode.is_some(), TraversalMode::CI => false, TraversalMode::Format { write, .. } => write, + TraversalMode::Migrate { write: dry_run, .. } => dry_run, } } @@ -155,6 +164,7 @@ impl Execution { TraversalMode::Check { .. } => "check", TraversalMode::CI { .. } => "ci", TraversalMode::Format { .. } => "format", + TraversalMode::Migrate { .. } => "migrate", } } } @@ -184,46 +194,22 @@ pub(crate) fn execute_mode( // In case of other commands that pass here, we limit to 50 to avoid to delay the terminal. match &mode.traversal_mode { TraversalMode::Check { .. } => 20, - TraversalMode::CI | TraversalMode::Format { .. } => 50, + TraversalMode::CI | TraversalMode::Format { .. } | TraversalMode::Migrate { .. } => 50, } }; // don't do any traversal if there's some content coming from stdin if let Some((path, content)) = mode.as_stdin_file() { - let workspace = &*session.app.workspace; - let console = &mut *session.app.console; let rome_path = RomePath::new(path); - - if mode.is_format() { - let unsupported_format_reason = workspace - .supports_feature(SupportsFeatureParams { - path: rome_path.clone(), - feature: FeatureName::Format, - })? - .reason; - if unsupported_format_reason.is_none() { - workspace.open_file(OpenFileParams { - path: rome_path.clone(), - version: 0, - content: content.into(), - language_hint: Language::default(), - })?; - let printed = workspace.format_file(FormatFileParams { path: rome_path })?; - - console.append(markup! { - {printed.as_code()} - }); - } else { - console.append(markup! { - {content} - }); - console.error(markup!{ - "The content was not formatted because the formatter is currently disabled." - }) - } - } - - Ok(()) + std_in::run(session, &mode, rome_path, content.as_str()) + } else if let TraversalMode::Migrate { + write, + configuration_path, + } = mode.traversal_mode + { + let verbose = session.args.contains("--verbose"); + + migrate::run(session, write, configuration_path, verbose) } else { traverse(mode, session) } diff --git a/crates/rome_cli/src/traversal/process_file.rs b/crates/rome_cli/src/execute/process_file.rs similarity index 97% rename from crates/rome_cli/src/traversal/process_file.rs rename to crates/rome_cli/src/execute/process_file.rs index c1b5f41f2bd..154375f7220 100644 --- a/crates/rome_cli/src/traversal/process_file.rs +++ b/crates/rome_cli/src/execute/process_file.rs @@ -1,7 +1,6 @@ +use crate::execute::diagnostics::{ResultExt, ResultIoExt, SkippedDiagnostic, UnhandledDiagnostic}; +use crate::execute::traverse::TraversalOptions; use crate::execute::TraversalMode; -use crate::traversal::{ - ResultExt, ResultIoExt, SkippedDiagnostic, TraversalOptions, UnhandledDiagnostic, -}; use crate::{CliDiagnostic, FormatterReportFileDetail}; use rome_diagnostics::{category, DiagnosticExt, Error}; use rome_fs::{OpenOptions, RomePath}; @@ -101,6 +100,7 @@ pub(crate) fn process_file(ctx: &TraversalOptions, path: &Path) -> FileResult { .and(supported_format.reason.as_ref()) .and(supported_organize_imports.reason.as_ref()), TraversalMode::Format { .. } => supported_format.reason.as_ref(), + TraversalMode::Migrate { .. } => None, }; if let Some(reason) = unsupported_reason { @@ -285,6 +285,7 @@ pub(crate) fn process_file(ctx: &TraversalOptions, path: &Path) -> FileResult { } TraversalMode::CI { .. } => false, TraversalMode::Format { write, .. } => *write, + TraversalMode::Migrate { write: dry_run, .. } => *dry_run, }; let printed = file_guard diff --git a/crates/rome_cli/src/execute/std_in.rs b/crates/rome_cli/src/execute/std_in.rs new file mode 100644 index 00000000000..9862df87e1a --- /dev/null +++ b/crates/rome_cli/src/execute/std_in.rs @@ -0,0 +1,49 @@ +//! In here, there are the operations that run via standard input +//! +use crate::execute::Execution; +use crate::{CliDiagnostic, CliSession}; +use rome_console::{markup, ConsoleExt}; +use rome_fs::RomePath; +use rome_service::workspace::{ + FeatureName, FormatFileParams, Language, OpenFileParams, SupportsFeatureParams, +}; + +pub(crate) fn run<'a>( + session: CliSession, + mode: &'a Execution, + rome_path: RomePath, + content: &'a str, +) -> Result<(), CliDiagnostic> { + let workspace = &*session.app.workspace; + let console = &mut *session.app.console; + + if mode.is_format() { + let unsupported_format_reason = workspace + .supports_feature(SupportsFeatureParams { + path: rome_path.clone(), + feature: FeatureName::Format, + })? + .reason; + if unsupported_format_reason.is_none() { + workspace.open_file(OpenFileParams { + path: rome_path.clone(), + version: 0, + content: content.into(), + language_hint: Language::default(), + })?; + let printed = workspace.format_file(FormatFileParams { path: rome_path })?; + + console.append(markup! { + {printed.as_code()} + }); + } else { + console.append(markup! { + {content} + }); + console.error(markup!{ + "The content was not formatted because the formatter is currently disabled." + }) + } + } + Ok(()) +} diff --git a/crates/rome_cli/src/traversal/mod.rs b/crates/rome_cli/src/execute/traverse.rs similarity index 85% rename from crates/rome_cli/src/traversal/mod.rs rename to crates/rome_cli/src/execute/traverse.rs index d810abcad59..8b30a23d49c 100644 --- a/crates/rome_cli/src/traversal/mod.rs +++ b/crates/rome_cli/src/execute/traverse.rs @@ -1,6 +1,8 @@ -mod process_file; - -use crate::traversal::process_file::DiffKind; +use super::process_file::{process_file, DiffKind, FileStatus, Message}; +use crate::execute::diagnostics::{ + CIFormatDiffDiagnostic, CIOrganizeImportsDiffDiagnostic, ContentDiffAdvice, + FormatDiffDiagnostic, OrganizeImportsDiffDiagnostic, PanicDiagnostic, +}; use crate::{ CliDiagnostic, CliSession, Execution, FormatterReportFileDetail, FormatterReportSummary, Report, ReportDiagnostic, ReportDiff, ReportErrorKind, ReportKind, TraversalMode, @@ -9,12 +11,10 @@ use crossbeam::{ channel::{unbounded, Receiver, Sender}, select, }; -use process_file::{process_file, FileStatus, Message}; use rome_console::{fmt, markup, Console, ConsoleExt}; use rome_diagnostics::{ - adapters::{IoError, StdError}, - category, Advices, Category, Diagnostic, DiagnosticExt, Error, PrintDescription, - PrintDiagnostic, Resource, Severity, Visit, + adapters::StdError, category, DiagnosticExt, Error, PrintDescription, PrintDiagnostic, + Resource, Severity, }; use rome_fs::{FileSystem, PathInterner, RomePath}; use rome_fs::{TraversalContext, TraversalScope}; @@ -23,7 +23,6 @@ use rome_service::{ workspace::{FeatureName, SupportsFeatureParams}, Workspace, WorkspaceError, }; -use rome_text_edit::TextEdit; use std::collections::HashSet; use std::{ ffi::OsString, @@ -181,6 +180,18 @@ pub(crate) fn traverse(execution: Execution, mut session: CliSession) -> Result< "Formatted "{count}" file(s) in "{duration} }); } + + TraversalMode::Migrate { write: false, .. } => { + console.log(markup! { + "Checked your configuration file in "{duration} + }); + } + + TraversalMode::Migrate { write: true, .. } => { + console.log(markup! { + "Migrated your configuration file in "{duration} + }); + } } } else { if let TraversalMode::Format { write, .. } = execution.traversal_mode() { @@ -273,82 +284,6 @@ struct ProcessMessagesOptions<'ctx> { verbose: bool, } -#[derive(Debug, Diagnostic)] -#[diagnostic( - category = "format", - message = "File content differs from formatting output" -)] -struct CIFormatDiffDiagnostic<'a> { - #[location(resource)] - file_name: &'a str, - #[advice] - diff: FormatDiffAdvice<'a>, -} - -#[derive(Debug, Diagnostic)] -#[diagnostic( - category = "organizeImports", - message = "Import statements differs from the output" -)] -struct CIOrganizeImportsDiffDiagnostic<'a> { - #[location(resource)] - file_name: &'a str, - #[advice] - diff: FormatDiffAdvice<'a>, -} - -#[derive(Debug, Diagnostic)] -#[diagnostic( - category = "format", - severity = Information, - message = "Formatter would have printed the following content:" -)] -struct FormatDiffDiagnostic<'a> { - #[location(resource)] - file_name: &'a str, - #[advice] - diff: FormatDiffAdvice<'a>, -} - -#[derive(Debug, Diagnostic)] -#[diagnostic( - category = "organizeImports", - severity = Information, - message = "Import statements could be sorted:" -)] -struct OrganizeImportsDiffDiagnostic<'a> { - #[location(resource)] - file_name: &'a str, - #[advice] - diff: FormatDiffAdvice<'a>, -} - -#[derive(Debug)] -struct FormatDiffAdvice<'a> { - old: &'a str, - new: &'a str, -} - -impl Advices for FormatDiffAdvice<'_> { - fn record(&self, visitor: &mut dyn Visit) -> io::Result<()> { - let diff = TextEdit::from_unicode_words(self.old, self.new); - visitor.record_diff(&diff) - } -} - -#[derive(Debug, Diagnostic)] -struct TraversalDiagnostic<'a> { - #[location(resource)] - file_name: Option<&'a str>, - #[severity] - severity: Severity, - #[category] - category: &'static Category, - #[message] - #[description] - message: &'a str, -} - /// This thread receives [Message]s from the workers through the `recv_msgs` /// and `recv_files` channels and handles them based on [Execution] fn process_messages(options: ProcessMessagesOptions) { @@ -576,7 +511,7 @@ fn process_messages(options: ProcessMessagesOptions) { DiffKind::Format => { let diag = CIFormatDiffDiagnostic { file_name: &file_name, - diff: FormatDiffAdvice { + diff: ContentDiffAdvice { old: &old, new: &new, }, @@ -588,7 +523,7 @@ fn process_messages(options: ProcessMessagesOptions) { DiffKind::OrganizeImports => { let diag = CIOrganizeImportsDiffDiagnostic { file_name: &file_name, - diff: FormatDiffAdvice { + diff: ContentDiffAdvice { old: &old, new: &new, }, @@ -603,7 +538,7 @@ fn process_messages(options: ProcessMessagesOptions) { DiffKind::Format => { let diag = FormatDiffDiagnostic { file_name: &file_name, - diff: FormatDiffAdvice { + diff: ContentDiffAdvice { old: &old, new: &new, }, @@ -615,7 +550,7 @@ fn process_messages(options: ProcessMessagesOptions) { DiffKind::OrganizeImports => { let diag = OrganizeImportsDiffDiagnostic { file_name: &file_name, - diff: FormatDiffAdvice { + diff: ContentDiffAdvice { old: &old, new: &new, }, @@ -659,11 +594,11 @@ fn process_messages(options: ProcessMessagesOptions) { /// Context object shared between directory traversal tasks pub(crate) struct TraversalOptions<'ctx, 'app> { /// Shared instance of [FileSystem] - fs: &'app dyn FileSystem, + pub(crate) fs: &'app dyn FileSystem, /// Instance of [Workspace] used by this instance of the CLI - workspace: &'ctx dyn Workspace, + pub(crate) workspace: &'ctx dyn Workspace, /// Determines how the files should be processed - execution: &'ctx Execution, + pub(crate) execution: &'ctx Execution, /// File paths interner cache used by the filesystem traversal interner: PathInterner, /// Shared atomic counter storing the number of processed files @@ -671,45 +606,51 @@ pub(crate) struct TraversalOptions<'ctx, 'app> { /// Shared atomic counter storing the number of skipped files skipped: &'ctx AtomicUsize, /// Channel sending messages to the display thread - messages: Sender, + pub(crate) messages: Sender, /// Channel sending reports to the reports thread sender_reports: Sender, /// The approximate number of diagnostics the console will print before /// folding the rest into the "skipped diagnostics" counter - remaining_diagnostics: &'ctx AtomicU16, + pub(crate) remaining_diagnostics: &'ctx AtomicU16, } impl<'ctx, 'app> TraversalOptions<'ctx, 'app> { - fn increment_processed(&self) { + pub(crate) fn increment_processed(&self) { self.processed.fetch_add(1, Ordering::Relaxed); } /// Send a message to the display thread - fn push_message(&self, msg: impl Into) { + pub(crate) fn push_message(&self, msg: impl Into) { self.messages.send(msg.into()).ok(); } - fn can_format(&self, rome_path: &RomePath) -> Result { + pub(crate) fn can_format( + &self, + rome_path: &RomePath, + ) -> Result { self.workspace.supports_feature(SupportsFeatureParams { path: rome_path.clone(), feature: FeatureName::Format, }) } - fn push_format_stat(&self, path: String, stat: FormatterReportFileDetail) { + pub(crate) fn push_format_stat(&self, path: String, stat: FormatterReportFileDetail) { self.sender_reports .send(ReportKind::Formatter(path, stat)) .ok(); } - fn can_lint(&self, rome_path: &RomePath) -> Result { + pub(crate) fn can_lint( + &self, + rome_path: &RomePath, + ) -> Result { self.workspace.supports_feature(SupportsFeatureParams { path: rome_path.clone(), feature: FeatureName::Lint, }) } - fn can_organize_imports( + pub(crate) fn can_organize_imports( &self, rome_path: &RomePath, ) -> Result { @@ -719,7 +660,7 @@ impl<'ctx, 'app> TraversalOptions<'ctx, 'app> { }) } - fn miss_handler_err(&self, err: WorkspaceError, rome_path: &RomePath) { + pub(crate) fn miss_handler_err(&self, err: WorkspaceError, rome_path: &RomePath) { self.push_diagnostic( StdError::from(err) .with_category(category!("files/missingHandler")) @@ -781,6 +722,8 @@ impl<'ctx, 'app> TraversalContext for TraversalOptions<'ctx, 'app> { self.miss_handler_err(err, rome_path); false }), + // Imagine if Rome can't handle its own configuration file... + TraversalMode::Migrate { .. } => true, } } @@ -818,59 +761,3 @@ fn handle_file(ctx: &TraversalOptions, path: &Path) { } } } - -#[derive(Debug, Diagnostic)] -#[diagnostic(category = "internalError/panic", tags(INTERNAL))] -struct PanicDiagnostic { - #[description] - #[message] - message: String, -} - -#[derive(Debug, Diagnostic)] -#[diagnostic(category = "files/missingHandler", message = "unhandled file type")] -struct UnhandledDiagnostic; - -#[derive(Debug, Diagnostic)] -#[diagnostic(category = "parse", message = "Skipped file with syntax errors")] -struct SkippedDiagnostic; - -/// Extension trait for turning [Display]-able error types into [TraversalError] -trait ResultExt { - type Result; - fn with_file_path_and_code( - self, - file_path: String, - code: &'static Category, - ) -> Result; -} - -impl ResultExt for Result -where - E: std::error::Error + Send + Sync + 'static, -{ - type Result = T; - - fn with_file_path_and_code( - self, - file_path: String, - code: &'static Category, - ) -> Result { - self.map_err(move |err| { - StdError::from(err) - .with_category(code) - .with_file_path(file_path) - }) - } -} - -/// Extension trait for turning [io::Error] into [Error] -trait ResultIoExt: ResultExt { - fn with_file_path(self, file_path: String) -> Result; -} - -impl ResultIoExt for io::Result { - fn with_file_path(self, file_path: String) -> Result { - self.map_err(|error| IoError::from(error).with_file_path(file_path)) - } -} diff --git a/crates/rome_cli/src/lib.rs b/crates/rome_cli/src/lib.rs index 5b2e3284b15..506850d4544 100644 --- a/crates/rome_cli/src/lib.rs +++ b/crates/rome_cli/src/lib.rs @@ -22,7 +22,6 @@ mod panic; mod parse_arguments; mod reports; mod service; -mod traversal; pub use diagnostics::CliDiagnostic; pub(crate) use execute::{execute_mode, Execution, TraversalMode}; @@ -89,6 +88,7 @@ impl<'app> CliSession<'app> { Some("start") => commands::daemon::start(self), Some("stop") => commands::daemon::stop(self), Some("lsp-proxy") => commands::daemon::lsp_proxy(), + Some("migrate") => commands::migrate::migrate(self), // Internal commands Some("__run_server") => commands::daemon::run_server(self), diff --git a/crates/rome_cli/tests/commands/migrate.rs b/crates/rome_cli/tests/commands/migrate.rs new file mode 100644 index 00000000000..4e34f0f85ad --- /dev/null +++ b/crates/rome_cli/tests/commands/migrate.rs @@ -0,0 +1,66 @@ +use crate::run_cli; +use crate::snap_test::{assert_cli_snapshot, SnapshotPayload}; +use pico_args::Arguments; +use rome_console::BufferConsole; +use rome_fs::{FileSystemExt, MemoryFileSystem}; +use rome_service::DynRef; +use std::ffi::OsString; +use std::path::Path; + +#[test] +fn migrate_config_up_to_date() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let configuration = r#"{ "linter": { "enabled": true } }"#; + + let configuration_path = Path::new("rome.json"); + fs.insert(configuration_path.into(), configuration.as_bytes()); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Arguments::from_vec(vec![OsString::from("migrate")]), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let mut file = fs.open(configuration_path).expect("file to open"); + + let mut content = String::new(); + file.read_to_string(&mut content) + .expect("failed to read file from memory FS"); + + assert_eq!(content, configuration); + + drop(file); + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "migrate_config_up_to_date", + fs, + console, + result, + )); +} + +#[test] +fn missing_configuration_file() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let result = run_cli( + DynRef::Borrowed(&mut fs), + &mut console, + Arguments::from_vec(vec![OsString::from("migrate")]), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "missing_configuration_file", + fs, + console, + result, + )); +} diff --git a/crates/rome_cli/tests/commands/mod.rs b/crates/rome_cli/tests/commands/mod.rs index 9a4f983b0f1..355c1d5b94a 100644 --- a/crates/rome_cli/tests/commands/mod.rs +++ b/crates/rome_cli/tests/commands/mod.rs @@ -2,5 +2,6 @@ mod check; mod ci; mod format; mod init; +mod migrate; mod rage; mod version; diff --git a/crates/rome_cli/tests/snapshots/main_commands_migrate/migrate_config_up_to_date.snap b/crates/rome_cli/tests/snapshots/main_commands_migrate/migrate_config_up_to_date.snap new file mode 100644 index 00000000000..3fe4855676b --- /dev/null +++ b/crates/rome_cli/tests/snapshots/main_commands_migrate/migrate_config_up_to_date.snap @@ -0,0 +1,17 @@ +--- +source: crates/rome_cli/tests/snap_test.rs +expression: content +--- +## `rome.json` + +```json +{ "linter": { "enabled": true } } +``` + +# Emitted Messages + +```block +Your configuration file is up to date. +``` + + diff --git a/crates/rome_cli/tests/snapshots/main_commands_migrate/missing_configuration_file.snap b/crates/rome_cli/tests/snapshots/main_commands_migrate/missing_configuration_file.snap new file mode 100644 index 00000000000..6950c9c15b6 --- /dev/null +++ b/crates/rome_cli/tests/snapshots/main_commands_migrate/missing_configuration_file.snap @@ -0,0 +1,16 @@ +--- +source: crates/rome_cli/tests/snap_test.rs +expression: content +--- +# Termination Message + +```block +migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Migration has encountered an error: Rome couldn't find the configuration file + + + +``` + + diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index d5938135481..98f9f8ed62d 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -174,6 +174,7 @@ define_categories! { "format", "configuration", "organizeImports", + "migrate", "deserialize", "internalError/io", "internalError/fs", diff --git a/crates/rome_js_analyze/Cargo.toml b/crates/rome_js_analyze/Cargo.toml index 9ae98c354a6..98fbbee2443 100644 --- a/crates/rome_js_analyze/Cargo.toml +++ b/crates/rome_js_analyze/Cargo.toml @@ -23,7 +23,7 @@ roaring = "0.10.1" rustc-hash = { workspace = true } serde = { version = "1.0.136", features = ["derive"] } serde_json = { version = "1.0.74", features = ["raw_value"] } -lazy_static = "1.4.0" +lazy_static = { workspace = true } natord = "1.0.9" [dev-dependencies] diff --git a/crates/rome_lsp/src/session.rs b/crates/rome_lsp/src/session.rs index bab8b1e7cdd..46f9fcb8b4a 100644 --- a/crates/rome_lsp/src/session.rs +++ b/crates/rome_lsp/src/session.rs @@ -381,7 +381,7 @@ impl Session { }; let status = match load_config(&self.fs, base_path) { - Ok(Some(deserialized)) => { + Ok(Some((deserialized, _))) => { let (configuration, diagnostics) = deserialized.consume(); if diagnostics.is_empty() { warn!("The deserialization of the configuration resulted in errors. Rome will its defaults where possible."); diff --git a/crates/rome_migrate/Cargo.toml b/crates/rome_migrate/Cargo.toml new file mode 100644 index 00000000000..7046f3773e2 --- /dev/null +++ b/crates/rome_migrate/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rome_migrate" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rome_rowan = { path = "../rome_rowan" } +rome_json_syntax = { path = "../rome_json_syntax"} +rome_json_parser = { path = "../rome_json_parser"} +rome_analyze = { path = "../rome_analyze"} +rome_diagnostics = { path = "../rome_diagnostics"} +lazy_static = { workspace = true } diff --git a/crates/rome_migrate/src/analyzers.rs b/crates/rome_migrate/src/analyzers.rs new file mode 100644 index 00000000000..c468c5a6a0d --- /dev/null +++ b/crates/rome_migrate/src/analyzers.rs @@ -0,0 +1,29 @@ +use crate::analyzers::rule_set::RuleSet; +use rome_analyze::{GroupCategory, RegistryVisitor, RuleCategory, RuleGroup}; +use rome_json_syntax::JsonLanguage; + +mod rule_set; + +pub(crate) struct MigrationGroup; +pub(crate) struct MigrationCategory; + +impl RuleGroup for MigrationGroup { + type Language = JsonLanguage; + type Category = MigrationCategory; + const NAME: &'static str = "migrations"; + + fn record_rules + ?Sized>(registry: &mut V) { + // Order here is important, rules should be added from the most old, to the most recent + // v13.0.0 + registry.record_rule::(); + } +} + +impl GroupCategory for MigrationCategory { + type Language = JsonLanguage; + const CATEGORY: RuleCategory = RuleCategory::Action; + + fn record_groups + ?Sized>(registry: &mut V) { + registry.record_group::(); + } +} diff --git a/crates/rome_migrate/src/analyzers/rule_set.rs b/crates/rome_migrate/src/analyzers/rule_set.rs new file mode 100644 index 00000000000..69e079edc75 --- /dev/null +++ b/crates/rome_migrate/src/analyzers/rule_set.rs @@ -0,0 +1,25 @@ +use crate::declare_migration; +use rome_analyze::context::RuleContext; +use rome_analyze::{Ast, Rule}; +use rome_json_syntax::JsonObjectValue; + +declare_migration! { + pub(crate) RuleSet { + version: "11.0.0", + name: "ruleSet", + } +} + +impl Rule for RuleSet { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = (); + + fn run(_: &RuleContext) -> Self::Signals { + // TODO: write rule to create a "ruleSet" config + // ruleSet -> "recommended", "all", "none" as a starter + // It should merge "recommended" and "all" + None + } +} diff --git a/crates/rome_migrate/src/lib.rs b/crates/rome_migrate/src/lib.rs new file mode 100644 index 00000000000..3fa2af4c023 --- /dev/null +++ b/crates/rome_migrate/src/lib.rs @@ -0,0 +1,123 @@ +mod analyzers; +mod macros; +mod registry; + +use crate::registry::visit_migration_registry; +pub use rome_analyze::ControlFlow; +use rome_analyze::{ + AnalysisFilter, Analyzer, AnalyzerContext, AnalyzerOptions, AnalyzerSignal, InspectMatcher, + LanguageRoot, MatchQueryParams, MetadataRegistry, RuleRegistry, +}; +use rome_diagnostics::Error; +use rome_json_syntax::JsonLanguage; +use std::convert::Infallible; +use std::path::Path; + +/// Return the static [MetadataRegistry] for the JS analyzer rules +pub fn metadata() -> &'static MetadataRegistry { + lazy_static::lazy_static! { + static ref METADATA: MetadataRegistry = { + let mut metadata = MetadataRegistry::default(); + visit_migration_registry(&mut metadata); + metadata + }; + } + + &METADATA +} + +/// Run the analyzer on the provided `root`: this process will use the given `filter` +/// to selectively restrict analysis to specific rules / a specific source range, +/// then call `emit_signal` when an analysis rule emits a diagnostic or action. +/// Additionally, this function takes a `inspect_matcher` function that can be +/// used to inspect the "query matches" emitted by the analyzer before they are +/// processed by the lint rules registry +pub fn analyze_with_inspect_matcher<'a, V, F, B>( + root: &LanguageRoot, + configuration_file_path: &'a Path, + inspect_matcher: V, + mut emit_signal: F, +) -> (Option, Vec) +where + V: FnMut(&MatchQueryParams) + 'a, + F: FnMut(&dyn AnalyzerSignal) -> ControlFlow + 'a, + B: 'a, +{ + let filter = AnalysisFilter::default(); + let options = AnalyzerOptions::default(); + let mut registry = RuleRegistry::builder(&filter, &options, root); + visit_migration_registry(&mut registry); + + let (migration_registry, services, diagnostics, visitors) = registry.build(); + + // Bail if we can't parse a rule option + if !diagnostics.is_empty() { + return (None, diagnostics); + } + + let mut analyzer = Analyzer::new( + metadata(), + InspectMatcher::new(migration_registry, inspect_matcher), + |_| -> Vec> { unreachable!() }, + |_| {}, + &mut emit_signal, + ); + + for ((phase, _), visitor) in visitors { + analyzer.add_visitor(phase, visitor); + } + + let globals: Vec<_> = options + .configuration + .globals + .iter() + .map(|global| global.as_str()) + .collect(); + ( + analyzer.run(AnalyzerContext { + root: root.clone(), + range: filter.range, + services, + globals: globals.as_slice(), + file_path: configuration_file_path, + }), + diagnostics, + ) +} + +pub fn migrate_configuration<'a, F, B>( + root: &LanguageRoot, + configuration_file_path: &'a Path, + emit_signal: F, +) -> (Option, Vec) +where + F: FnMut(&dyn AnalyzerSignal) -> ControlFlow + 'a, + B: 'a, +{ + analyze_with_inspect_matcher(root, configuration_file_path, |_| {}, emit_signal) +} + +#[cfg(test)] +mod test { + use crate::migrate_configuration; + use rome_analyze::{ControlFlow, Never}; + use rome_json_parser::parse_json; + use std::path::Path; + + #[test] + #[ignore] + fn smoke() { + let source = r#"{ "something": "else" }"#; + + let parsed = parse_json(source); + + migrate_configuration(&parsed.tree().value().unwrap(), Path::new(""), |signal| { + for action in signal.actions() { + let new_code = action.mutation.commit(); + eprintln!("{new_code}"); + } + + ControlFlow::::Continue(()) + }); + } +} diff --git a/crates/rome_migrate/src/macros.rs b/crates/rome_migrate/src/macros.rs new file mode 100644 index 00000000000..77c387c5ce6 --- /dev/null +++ b/crates/rome_migrate/src/macros.rs @@ -0,0 +1,26 @@ +#[macro_export] +macro_rules! declare_migration { + ( $vis:vis $id:ident { + version: $version:literal, + name: $name:tt, + $( $key:ident: $value:expr, )* + } ) => { + $vis enum $id {} + + impl rome_analyze::RuleMeta for $id { + type Group = $crate::analyzers::MigrationGroup; + const METADATA: rome_analyze::RuleMetadata = + rome_analyze::RuleMetadata::new($version, $name, "") $( .$key($value) )*; + } + + // Declare a new `rule_category!` macro in the module context that + // expands to the category of this rule + // This is implemented by calling the `group_category!` macro from the + // parent module (that should be declared by a call to `declare_group!`) + // and providing it with the name of this rule as a string literal token + #[allow(unused_macros)] + macro_rules! rule_category { + () => { super::group_category!( $name ) }; + } + }; +} diff --git a/crates/rome_migrate/src/registry.rs b/crates/rome_migrate/src/registry.rs new file mode 100644 index 00000000000..ca2e0c1c817 --- /dev/null +++ b/crates/rome_migrate/src/registry.rs @@ -0,0 +1,7 @@ +use crate::analyzers::MigrationCategory; +use rome_analyze::RegistryVisitor; +use rome_json_syntax::JsonLanguage; + +pub fn visit_migration_registry>(registry: &mut V) { + registry.record_category::(); +} diff --git a/crates/rome_service/src/configuration/mod.rs b/crates/rome_service/src/configuration/mod.rs index d3aae589a45..d9a58b06673 100644 --- a/crates/rome_service/src/configuration/mod.rs +++ b/crates/rome_service/src/configuration/mod.rs @@ -139,7 +139,8 @@ impl FilesConfiguration { /// - [Option]: sometimes not having a configuration file should not be an error, so we need this type. /// - [Deserialized]: result of the deserialization of the configuration. /// - [Configuration]: the type needed to [Deserialized] to infer the return type. -type LoadConfig = Result>, WorkspaceError>; +/// - [PathBuf]: the path of where the first `rome.json` path was found +type LoadConfig = Result, PathBuf)>, WorkspaceError>; #[derive(Debug, Default, PartialEq)] pub enum ConfigurationBasePath { @@ -210,7 +211,7 @@ pub fn load_config( let deserialized = deserialize_from_json_str::(&buffer) .with_file_path(&configuration_path.display().to_string()); - Ok(Some(deserialized)) + Ok(Some((deserialized, configuration_path))) } Err(err) => { // base paths from users are not eligible for auto discovery diff --git a/npm/backend-jsonrpc/src/workspace.ts b/npm/backend-jsonrpc/src/workspace.ts index 04624079b1a..dea96a33497 100644 --- a/npm/backend-jsonrpc/src/workspace.ts +++ b/npm/backend-jsonrpc/src/workspace.ts @@ -1022,6 +1022,7 @@ export type Category = | "format" | "configuration" | "organizeImports" + | "migrate" | "deserialize" | "internalError/io" | "internalError/fs"