From d804033e9eb6a59bbd8f1e5697cab5c85d8928d4 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Wed, 4 Oct 2023 08:34:20 +0100 Subject: [PATCH] feat(migrate): add migration for `indentWidth` (#476) --- Cargo.lock | 6 + crates/biome_json_factory/src/lib.rs | 5 +- crates/biome_json_factory/src/make.rs | 7 + crates/biome_migrate/Cargo.toml | 19 ++- crates/biome_migrate/src/analyzers.rs | 8 +- .../src/analyzers/indent_size.rs | 65 +++++++++ .../biome_migrate/src/analyzers/rule_set.rs | 25 ---- crates/biome_migrate/src/lib.rs | 4 +- crates/biome_migrate/tests/spec_tests.rs | 127 ++++++++++++++++++ .../specs/migrations/indentSize/invalid.json | 15 +++ .../migrations/indentSize/invalid.json.snap | 113 ++++++++++++++++ 11 files changed, 355 insertions(+), 39 deletions(-) create mode 100644 crates/biome_json_factory/src/make.rs create mode 100644 crates/biome_migrate/src/analyzers/indent_size.rs delete mode 100644 crates/biome_migrate/src/analyzers/rule_set.rs create mode 100644 crates/biome_migrate/tests/spec_tests.rs create mode 100644 crates/biome_migrate/tests/specs/migrations/indentSize/invalid.json create mode 100644 crates/biome_migrate/tests/specs/migrations/indentSize/invalid.json.snap diff --git a/Cargo.lock b/Cargo.lock index d1638a0356ae..eb7637c43f81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -626,11 +626,17 @@ name = "biome_migrate" version = "0.0.0" dependencies = [ "biome_analyze", + "biome_console", "biome_diagnostics", + "biome_json_factory", "biome_json_parser", "biome_json_syntax", "biome_rowan", + "biome_service", + "biome_test_utils", + "insta", "lazy_static", + "tests_macros", ] [[package]] diff --git a/crates/biome_json_factory/src/lib.rs b/crates/biome_json_factory/src/lib.rs index 698a81fd748e..84385867093c 100644 --- a/crates/biome_json_factory/src/lib.rs +++ b/crates/biome_json_factory/src/lib.rs @@ -1,13 +1,12 @@ -pub use crate::generated::JsonSyntaxFactory; use biome_json_syntax::JsonLanguage; use biome_rowan::TreeBuilder; mod generated; +pub use crate::generated::JsonSyntaxFactory; +pub mod make; // Re-exported for tests #[doc(hidden)] pub use biome_json_syntax as syntax; pub type JsonSyntaxTreeBuilder = TreeBuilder<'static, JsonLanguage, JsonSyntaxFactory>; - -pub use generated::node_factory as make; diff --git a/crates/biome_json_factory/src/make.rs b/crates/biome_json_factory/src/make.rs new file mode 100644 index 000000000000..fd5cb550cfec --- /dev/null +++ b/crates/biome_json_factory/src/make.rs @@ -0,0 +1,7 @@ +use biome_json_syntax::{JsonSyntaxKind, JsonSyntaxToken}; + +pub use crate::generated::node_factory::*; + +pub fn ident(text: &str) -> JsonSyntaxToken { + JsonSyntaxToken::new_detached(JsonSyntaxKind::IDENT, text, [], []) +} diff --git a/crates/biome_migrate/Cargo.toml b/crates/biome_migrate/Cargo.toml index 32f3de866282..573eb29d6fd6 100644 --- a/crates/biome_migrate/Cargo.toml +++ b/crates/biome_migrate/Cargo.toml @@ -14,11 +14,18 @@ version = "0.0.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -biome_analyze = { workspace = true } -biome_diagnostics = { workspace = true } -biome_json_syntax = { workspace = true } -biome_rowan = { workspace = true } -lazy_static = { workspace = true } +biome_analyze = { workspace = true } +biome_console = { workspace = true } +biome_diagnostics = { workspace = true } +biome_json_factory = { workspace = true } +biome_json_syntax = { workspace = true } +biome_rowan = { workspace = true } +lazy_static = { workspace = true } [dev-dependencies] -biome_json_parser = { workspace = true } +biome_json_factory = { path = "../biome_json_factory" } +biome_json_parser = { path = "../biome_json_parser" } +biome_service = { path = "../biome_service" } +biome_test_utils = { path = "../biome_test_utils" } +insta = { workspace = true, features = ["glob"] } +tests_macros = { path = "../tests_macros" } diff --git a/crates/biome_migrate/src/analyzers.rs b/crates/biome_migrate/src/analyzers.rs index 40be95b3308b..58fc8bc96f70 100644 --- a/crates/biome_migrate/src/analyzers.rs +++ b/crates/biome_migrate/src/analyzers.rs @@ -1,8 +1,8 @@ -use crate::analyzers::rule_set::RuleSet; +use crate::analyzers::indent_size::IndentSize; use biome_analyze::{GroupCategory, RegistryVisitor, RuleCategory, RuleGroup}; use biome_json_syntax::JsonLanguage; -mod rule_set; +mod indent_size; pub(crate) struct MigrationGroup; pub(crate) struct MigrationCategory; @@ -14,8 +14,8 @@ impl RuleGroup for MigrationGroup { 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::(); + // v1.3.0 + registry.record_rule::(); } } diff --git a/crates/biome_migrate/src/analyzers/indent_size.rs b/crates/biome_migrate/src/analyzers/indent_size.rs new file mode 100644 index 000000000000..4b92eb2d0a62 --- /dev/null +++ b/crates/biome_migrate/src/analyzers/indent_size.rs @@ -0,0 +1,65 @@ +use crate::{declare_migration, MigrationAction}; +use biome_analyze::context::RuleContext; +use biome_analyze::{ActionCategory, Ast, Rule, RuleAction, RuleDiagnostic}; +use biome_console::markup; +use biome_diagnostics::{category, Applicability}; +use biome_json_factory::make::{ident, json_member_name}; +use biome_json_syntax::JsonMemberName; +use biome_rowan::{AstNode, BatchMutationExt}; + +declare_migration! { + pub(crate) IndentSize { + version: "1.3.0", + name: "indentSize", + } +} + +impl Rule for IndentSize { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + + let node_text = node.inner_string_text().ok()?; + if node_text.text() == "indentSize" { + return Some(()); + } + None + } + + fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { + let node = ctx.query(); + Some( + RuleDiagnostic::new( + category!("migrate"), + node.range(), + markup! { + "The option ""indentSize"" is deprecated." + } + .to_owned(), + ) + .deprecated(), + ) + } + + fn action(ctx: &RuleContext, _state: &Self::State) -> Option { + let node = ctx.query(); + let mut mutation = ctx.root().begin(); + + let new_node = json_member_name(ident("\"indentWidth\"")); + mutation.replace_node(node.clone(), new_node); + + Some(RuleAction { + category: ActionCategory::QuickFix, + applicability: Applicability::Always, + message: markup! { + "Use the property ""indentWidth"" instead." + } + .to_owned(), + mutation, + }) + } +} diff --git a/crates/biome_migrate/src/analyzers/rule_set.rs b/crates/biome_migrate/src/analyzers/rule_set.rs deleted file mode 100644 index 50c039fa733c..000000000000 --- a/crates/biome_migrate/src/analyzers/rule_set.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::declare_migration; -use biome_analyze::context::RuleContext; -use biome_analyze::{Ast, Rule}; -use biome_json_syntax::JsonObjectValue; - -declare_migration! { - pub(crate) RuleSet { - version: "1.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/biome_migrate/src/lib.rs b/crates/biome_migrate/src/lib.rs index 9788028bb181..cf7c05786263 100644 --- a/crates/biome_migrate/src/lib.rs +++ b/crates/biome_migrate/src/lib.rs @@ -6,7 +6,7 @@ use crate::registry::visit_migration_registry; pub use biome_analyze::ControlFlow; use biome_analyze::{ AnalysisFilter, Analyzer, AnalyzerContext, AnalyzerOptions, AnalyzerSignal, InspectMatcher, - LanguageRoot, MatchQueryParams, MetadataRegistry, RuleRegistry, + LanguageRoot, MatchQueryParams, MetadataRegistry, RuleAction, RuleRegistry, }; use biome_diagnostics::Error; use biome_json_syntax::JsonLanguage; @@ -93,6 +93,8 @@ where analyze_with_inspect_matcher(root, configuration_file_path, |_| {}, emit_signal) } +pub(crate) type MigrationAction = RuleAction; + #[cfg(test)] mod test { use crate::migrate_configuration; diff --git a/crates/biome_migrate/tests/spec_tests.rs b/crates/biome_migrate/tests/spec_tests.rs new file mode 100644 index 000000000000..f1f68a4cd13d --- /dev/null +++ b/crates/biome_migrate/tests/spec_tests.rs @@ -0,0 +1,127 @@ +use biome_analyze::{AnalyzerAction, ControlFlow, Never}; +use biome_diagnostics::advice::CodeSuggestionAdvice; +use biome_diagnostics::{DiagnosticExt, Severity}; +use biome_json_parser::{parse_json, JsonParserOptions}; +use biome_json_syntax::JsonLanguage; +use biome_rowan::AstNode; +use biome_test_utils::{ + assert_errors_are_absent, code_fix_to_string, diagnostic_to_string, + has_bogus_nodes_or_empty_slots, parse_test_path, register_leak_checker, + write_analyzer_snapshot, +}; +use std::ffi::OsStr; +use std::fs::read_to_string; +use std::path::Path; + +tests_macros::gen_tests! {"tests/specs/**/*.json", crate::run_test, "module"} + +fn run_test(input: &'static str, _: &str, _: &str, _: &str) { + register_leak_checker(); + + let input_file = Path::new(input); + let file_name = input_file.file_name().and_then(OsStr::to_str).unwrap(); + + let (group, rule) = parse_test_path(input_file); + if rule == "specs" || rule == "suppression" { + panic!("the test file must be placed in the {rule}/// directory"); + } + if group == "specs" || group == "suppression" { + panic!("the test file must be placed in the {group}/{rule}// directory"); + } + + let mut snapshot = String::new(); + + let input_code = read_to_string(input_file) + .unwrap_or_else(|err| panic!("failed to read {:?}: {:?}", input_file, err)); + + let quantity_diagnostics = analyze_and_snap(&mut snapshot, &input_code, file_name, input_file); + + insta::with_settings!({ + prepend_module_to_snapshot => false, + snapshot_path => input_file.parent().unwrap(), + }, { + insta::assert_snapshot!(file_name, snapshot, file_name); + }); + + if input_code.contains("/* should not generate diagnostics */") && quantity_diagnostics > 0 { + panic!("This test should not generate diagnostics"); + } +} + +pub(crate) fn analyze_and_snap( + snapshot: &mut String, + input_code: &str, + file_name: &str, + input_file: &Path, +) -> usize { + let parsed = parse_json(input_code, JsonParserOptions::default()); + let root = parsed.tree(); + + let mut diagnostics = Vec::new(); + let mut code_fixes = Vec::new(); + + let (_, errors) = + biome_migrate::migrate_configuration(&root.value().unwrap(), input_file, |event| { + if let Some(mut diag) = event.diagnostic() { + for action in event.actions() { + if !action.is_suppression() { + check_code_action(input_file, input_code, &action); + diag = diag.add_code_suggestion(CodeSuggestionAdvice::from(action)); + } + } + + let error = diag.with_severity(Severity::Warning); + diagnostics.push(diagnostic_to_string(file_name, input_code, error)); + return ControlFlow::Continue(()); + } + + for action in event.actions() { + if !action.is_suppression() { + check_code_action(input_file, input_code, &action); + code_fixes.push(code_fix_to_string(input_code, action)); + } + } + + ControlFlow::::Continue(()) + }); + + for error in errors { + diagnostics.push(diagnostic_to_string(file_name, input_code, error)); + } + write_analyzer_snapshot( + snapshot, + input_code, + diagnostics.as_slice(), + code_fixes.as_slice(), + ); + + diagnostics.len() +} + +fn check_code_action(path: &Path, source: &str, action: &AnalyzerAction) { + let (_, text_edit) = action.mutation.as_text_edits().unwrap_or_default(); + + let output = text_edit.new_string(source); + + let new_tree = action.mutation.clone().commit(); + + // Checks that applying the text edits returned by the BatchMutation + // returns the same code as printing the modified syntax tree + assert_eq!(new_tree.to_string(), output); + + if has_bogus_nodes_or_empty_slots(&new_tree) { + panic!( + "modified tree has bogus nodes or empty slots:\n{new_tree:#?} \n\n {}", + new_tree + ) + } + + // Checks the returned tree contains no missing children node + if format!("{new_tree:?}").contains("missing (required)") { + panic!("modified tree has missing children:\n{new_tree:#?}") + } + + // Re-parse the modified code and panic if the resulting tree has syntax errors + let re_parse = parse_json(&output, JsonParserOptions::default()); + assert_errors_are_absent(re_parse.tree().syntax(), re_parse.diagnostics(), path); +} diff --git a/crates/biome_migrate/tests/specs/migrations/indentSize/invalid.json b/crates/biome_migrate/tests/specs/migrations/indentSize/invalid.json new file mode 100644 index 000000000000..472487ec704e --- /dev/null +++ b/crates/biome_migrate/tests/specs/migrations/indentSize/invalid.json @@ -0,0 +1,15 @@ +{ + "formatter": { + "indentSize": 4 + }, + "javascript": { + "formatter": { + "indentSize": 4 + } + }, + "json": { + "formatter": { + "indentSize": 4 + } + } +} diff --git a/crates/biome_migrate/tests/specs/migrations/indentSize/invalid.json.snap b/crates/biome_migrate/tests/specs/migrations/indentSize/invalid.json.snap new file mode 100644 index 000000000000..10a99741cfd9 --- /dev/null +++ b/crates/biome_migrate/tests/specs/migrations/indentSize/invalid.json.snap @@ -0,0 +1,113 @@ +--- +source: crates/biome_migrate/tests/spec_tests.rs +expression: invalid.json +--- +# Input +```js +{ + "formatter": { + "indentSize": 4 + }, + "javascript": { + "formatter": { + "indentSize": 4 + } + }, + "json": { + "formatter": { + "indentSize": 4 + } + } +} + +``` + +# Diagnostics +``` +invalid.json:3:3 migrate FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The option indentSize is deprecated. + + 1 │ { + 2 │ "formatter": { + > 3 │ "indentSize": 4 + │ ^^^^^^^^^^^^ + 4 │ }, + 5 │ "javascript": { + + i Safe fix: Use the property indentWidth instead. + + 1 1 │ { + 2 2 │ "formatter": { + 3 │ - → → "indentSize":·4 + 3 │ + → → "indentWidth":·4 + 4 4 │ }, + 5 5 │ "javascript": { + ····· │ + 13 13 │ } + 14 14 │ } + 15 │ - } + 15 │ + } + 16 │ + + + +``` + +``` +invalid.json:7:4 migrate FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The option indentSize is deprecated. + + 5 │ "javascript": { + 6 │ "formatter": { + > 7 │ "indentSize": 4 + │ ^^^^^^^^^^^^ + 8 │ } + 9 │ }, + + i Safe fix: Use the property indentWidth instead. + + 5 5 │ "javascript": { + 6 6 │ "formatter": { + 7 │ - → → → "indentSize":·4 + 7 │ + → → → "indentWidth":·4 + 8 8 │ } + 9 9 │ }, + ····· │ + 13 13 │ } + 14 14 │ } + 15 │ - } + 15 │ + } + 16 │ + + + +``` + +``` +invalid.json:12:4 migrate FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The option indentSize is deprecated. + + 10 │ "json": { + 11 │ "formatter": { + > 12 │ "indentSize": 4 + │ ^^^^^^^^^^^^ + 13 │ } + 14 │ } + + i Safe fix: Use the property indentWidth instead. + + 10 10 │ "json": { + 11 11 │ "formatter": { + 12 │ - → → → "indentSize":·4 + 12 │ + → → → "indentWidth":·4 + 13 13 │ } + 14 14 │ } + 15 │ - } + 15 │ + } + 16 │ + + + +``` + +