diff --git a/crates/biome_cli/tests/cases/migrate_v2.rs b/crates/biome_cli/tests/cases/migrate_v2.rs index 5db322b91d22..ac11b2bf1937 100644 --- a/crates/biome_cli/tests/cases/migrate_v2.rs +++ b/crates/biome_cli/tests/cases/migrate_v2.rs @@ -158,3 +158,309 @@ fn should_successfully_migrate_knip() { result, )); } + +#[test] +fn should_successfully_migrate_ariakit() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let configuration_path = Utf8Path::new("biome.json"); + fs.insert( + configuration_path.into(), + r#"{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "ignore": ["website/.next/**", "website/.pages/**", "**/*.css"] + }, + "organizeImports": { + "enabled": true + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "security": { + "noDangerouslySetInnerHtml": "off" + }, + "a11y": { + "noSvgWithoutTitle": "off", + "useButtonType": "off", + "useAnchorContent": "off", + "useValidAnchor": "off", + "useKeyWithClickEvents": "off", + "noAutofocus": "off", + "noLabelWithoutControl": "off", + "useSemanticElements": "off", + "useFocusableInteractive": "off" + }, + "suspicious": { + "noExplicitAny": "off", + "noShadowRestrictedNames": "off", + "noConfusingVoidType": "off", + "noArrayIndexKey": "off", + "noAssignInExpressions": "off" + }, + "correctness": { + "useExhaustiveDependencies": "off", + "useJsxKeyInIterable": "off" + }, + "style": { + "noParameterAssign": "off", + "noUnusedTemplateLiteral": "off", + "noNonNullAssertion": "off", + "noUselessElse": "off" + } + } + } +} +"# + .as_bytes(), + ); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from(["migrate", "--write"].as_slice()), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_successfully_migrate_ariakit", + fs, + console, + result, + )); +} + +#[test] +fn should_successfully_migrate_sentry() { + let mut fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let configuration_path = Utf8Path::new("biome.json"); + fs.insert( + configuration_path.into(), + r#"{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true, + "defaultBranch": "master" + }, + "organizeImports": { + "enabled": false + }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "a11y": { + "noBlankTarget": "error" + }, + "correctness": { + "noGlobalObjectCalls": "error", + "noUnreachable": "error", + "useIsNan": "error", + "noUnusedPrivateClassMembers": "error", + "noInvalidUseBeforeDeclaration": "error", + "noNodejsModules": "error" + }, + "complexity": { + "useFlatMap": "error", + "useOptionalChain": "error", + "noEmptyTypeParameters": "error", + "noUselessLoneBlockStatements": "error", + "noUselessEmptyExport": "error", + "noUselessConstructor": "error", + "noUselessTypeConstraint": "error", + "noExcessiveNestedTestSuites": "error" + }, + "nursery": { + "noRestrictedImports": { + "level": "warn", + "options": { + "paths": {} + } + } + }, + "performance": { + "noBarrelFile": "error" + }, + "security": { + "noDangerouslySetInnerHtmlWithChildren": "error" + }, + "suspicious": { + "noDebugger": "error", + "noDoubleEquals": "error", + "noDuplicateJsxProps": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noDuplicateCase": "error", + "noFallthroughSwitchClause": "error", + "noRedeclare": "error", + "noSparseArray": "error", + "noUnsafeDeclarationMerging": "error", + "noUnsafeNegation": "error", + "useIsArray": "error", + "noApproximativeNumericConstant": "error", + "noMisrefactoredShorthandAssign": "error", + "useAwait": "error", + "useNamespaceKeyword": "error", + "noFocusedTests": "error", + "noDuplicateTestHooks": "error" + }, + "style": { + "noCommaOperator": "error", + "noShoutyConstants": "error", + "noParameterProperties": "error", + "noVar": "error", + "useConst": "error", + "useShorthandFunctionType": "error", + "useExportType": "error", + "useImportType": "error", + "useNodejsImportProtocol": "error", + "useLiteralEnumMembers": "error", + "useEnumInitializers": "error", + "useAsConstAssertion": "error", + "useBlockStatements": "error" + } + } + }, + "files": { + "ignoreUnknown": true, + "ignore": [ + "**/*/trace.json", + "static/app/data/world.json", + "**/*.sourcemap.js", + "**/*.min.js", + "fixtures", + ".devenv", + "package.json" + ] + }, + "css": { + "formatter": { + "enabled": false + }, + "linter": { + "enabled": false + } + }, + "formatter": { + "enabled": true, + "formatWithErrors": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "ignore": ["tests/**/*.json"] + }, + "javascript": { + "formatter": { + "enabled": false, + "lineWidth": 90, + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "es5", + "semicolons": "always", + "arrowParentheses": "asNeeded", + "bracketSpacing": false, + "bracketSameLine": false + } + }, + "json": { + "formatter": { + "enabled": true + }, + "parser": { + "allowComments": true, + "allowTrailingCommas": true + } + }, + "overrides": [ + { + "include": [ + "api-docs/*.ts", + "build-utils/*.ts", + "config/*.ts", + "scripts", + "tests/js/sentry-test/loadFixtures.ts", + "tests/js/jest-pegjs-transform.js", + "tests/js/setup.ts", + "tests/js/test-balancer/index.js", + "*.config.ts" + ], + "linter": { + "rules": { + "correctness": { + "noNodejsModules": "off" + } + } + } + }, + { + "include": ["src/sentry/templates/sentry/error-page-embed.js"], + "linter": { + "rules": { + "style": { + "noVar": "off" + } + } + } + }, + { + "include": [ + "static/app/utils/replays/types.tsx", + "static/app/utils/queryClient.tsx", + "static/app/views/performance/traceDetails/styles.tsx", + "static/app/icons/index.tsx", + "static/app/components/tabs/index.tsx", + "static/app/components/sparklines/line.tsx", + "static/app/types/index.tsx", + "tests/js/sentry-test/reactTestingLibrary.tsx", + "tests/js/sentry-test/index.tsx" + ], + "linter": { + "rules": { + "performance": { + "noBarrelFile": "off" + } + } + } + } + ] +} + +"# + .as_bytes(), + ); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from(["migrate", "--write"].as_slice()), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_successfully_migrate_sentry", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/snapshots/main_cases_migrate_v2/should_successfully_migrate_ariakit.snap b/crates/biome_cli/tests/snapshots/main_cases_migrate_v2/should_successfully_migrate_ariakit.snap new file mode 100644 index 000000000000..a2f2fb200349 --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_cases_migrate_v2/should_successfully_migrate_ariakit.snap @@ -0,0 +1,93 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +expression: redactor(content) +snapshot_kind: text +--- +## `biome.json` + +```json +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "**", + "!**/website/.next/**", + "!**/website/.pages/**", + "!**/*.css" + ] + }, + "organizeImports": { + "enabled": true + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "rules": { + "style": { + "noParameterAssign": "off", + "noUnusedTemplateLiteral": "off", + "noNonNullAssertion": "off", + "noUselessElse": "off", + "useLiteralEnumMembers": "error", + "noArguments": "error", + "useShorthandFunctionType": "error", + "useExportType": "error", + "useDefaultParameterLast": "error", + "noCommaOperator": "error", + "useSingleVarDeclarator": "error", + "useNodejsImportProtocol": "error", + "useConst": "error", + "noInferrableTypes": "error", + "useExponentiationOperator": "error", + "useSelfClosingElements": "error", + "useImportType": "error", + "useNumberNamespace": "error", + "useAsConstAssertion": "error", + "useNumericLiterals": "error", + "useTemplate": "error", + "useEnumInitializers": "error" + }, + "security": { + "noDangerouslySetInnerHtml": "off" + }, + "suspicious": { + "noExplicitAny": "off", + "noShadowRestrictedNames": "off", + "noConfusingVoidType": "off", + "noArrayIndexKey": "off", + "noAssignInExpressions": "off" + }, + "a11y": { + "noSvgWithoutTitle": "off", + "useButtonType": "off", + "useAnchorContent": "off", + "useValidAnchor": "off", + "useKeyWithClickEvents": "off", + "noAutofocus": "off", + "noLabelWithoutControl": "off", + "useSemanticElements": "off", + "useFocusableInteractive": "off" + }, + "correctness": { + "useExhaustiveDependencies": "off", + "useJsxKeyInIterable": "off" + } + } + } +} +``` + +# Emitted Messages + +```block +The configuration biome.json has been successfully migrated. +``` diff --git a/crates/biome_cli/tests/snapshots/main_cases_migrate_v2/should_successfully_migrate_knip.snap b/crates/biome_cli/tests/snapshots/main_cases_migrate_v2/should_successfully_migrate_knip.snap index 69e54a050ea6..5958dd4f4128 100644 --- a/crates/biome_cli/tests/snapshots/main_cases_migrate_v2/should_successfully_migrate_knip.snap +++ b/crates/biome_cli/tests/snapshots/main_cases_migrate_v2/should_successfully_migrate_knip.snap @@ -138,8 +138,10 @@ snapshot_kind: text { "includes": ["**/packages/knip/fixtures/**"], "assist": { - "source": { - "organizeImports": "off" + "actions": { + "source": { + "organizeImports": "off" + } } }, "linter": { diff --git a/crates/biome_cli/tests/snapshots/main_cases_migrate_v2/should_successfully_migrate_sentry.snap b/crates/biome_cli/tests/snapshots/main_cases_migrate_v2/should_successfully_migrate_sentry.snap new file mode 100644 index 000000000000..0e13eb29618c --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_cases_migrate_v2/should_successfully_migrate_sentry.snap @@ -0,0 +1,187 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +expression: redactor(content) +snapshot_kind: text +--- +## `biome.json` + +```json +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true, + "defaultBranch": "master" + }, + "assist": { + "actions": { + "source": { + "organizeImports": "off" + } + } + }, + "linter": { + "enabled": true, + "rules": { + "style": { + "useLiteralEnumMembers": "error", + "noArguments": "error", + "noParameterAssign": "error", + "useShorthandFunctionType": "error", + "useExportType": "error", + "useDefaultParameterLast": "error", + "noCommaOperator": "error", + "useSingleVarDeclarator": "error", + "useNodejsImportProtocol": "error", + "useConst": "error", + "noNonNullAssertion": "error", + "noInferrableTypes": "error", + "useExponentiationOperator": "error", + "noUselessElse": "error", + "useSelfClosingElements": "error", + "useImportType": "error", + "useNumberNamespace": "error", + "useAsConstAssertion": "error", + "noUnusedTemplateLiteral": "error", + "useNumericLiterals": "error", + "useTemplate": "error", + "useEnumInitializers": "error" + }, + "suspicious": { + "noDebugger": "error", + "noDoubleEquals": "error", + "noDuplicateJsxProps": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noDuplicateCase": "error", + "noFallthroughSwitchClause": "error", + "noRedeclare": "error", + "noSparseArray": "error", + "noUnsafeDeclarationMerging": "error", + "noUnsafeNegation": "error", + "useIsArray": "error", + "noApproximativeNumericConstant": "error", + "noMisrefactoredShorthandAssign": "error", + "useAwait": "error", + "useNamespaceKeyword": "error", + "noFocusedTests": "error", + "noDuplicateTestHooks": "error",,,,,,,,, + } + } + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "**", + "!**/*/trace.json", + "!**/static/app/data/world.json", + "!**/*.sourcemap.js", + "!**/*.min.js", + "!**/fixtures", + "!**/.devenv", + "!**/package.json" + ] + }, + "css": { + "formatter": { + "enabled": false + }, + "linter": { + "enabled": false + } + }, + "formatter": { + "enabled": true, + "formatWithErrors": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "includes": ["**", "!**/tests/**/*.json"] + }, + "javascript": { + "formatter": { + "enabled": false, + "lineWidth": 90, + "quoteStyle": "single", + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "es5", + "semicolons": "always", + "arrowParentheses": "asNeeded", + "bracketSpacing": false, + "bracketSameLine": false + } + }, + "json": { + "formatter": { + "enabled": true + }, + "parser": { + "allowComments": true, + "allowTrailingCommas": true + } + }, + "overrides": [ + { + "includes": [ + "**/api-docs/*.ts", + "**/build-utils/*.ts", + "**/config/*.ts", + "**/scripts/**", + "**/tests/js/sentry-test/loadFixtures.ts", + "**/tests/js/jest-pegjs-transform.js", + "**/tests/js/setup.ts", + "**/tests/js/test-balancer/index.js", + "**/*.config.ts" + ], + "linter": { + "rules": { + "correctness": { + "noNodejsModules": "off" + } + } + } + }, + { + "includes": ["**/src/sentry/templates/sentry/error-page-embed.js"], + "linter": { + "rules": { + "suspicious": { + "noVar": "off" + }, + "suspicious": { + "noVar": "off" + } + } + } + }, + { + "includes": [ + "**/static/app/utils/replays/types.tsx", + "**/static/app/utils/queryClient.tsx", + "**/static/app/views/performance/traceDetails/styles.tsx", + "**/static/app/icons/index.tsx", + "**/static/app/components/tabs/index.tsx", + "**/static/app/components/sparklines/line.tsx", + "**/static/app/types/index.tsx", + "**/tests/js/sentry-test/reactTestingLibrary.tsx", + "**/tests/js/sentry-test/index.tsx" + ], + "linter": { + "rules": { + "performance": { + "noBarrelFile": "off" + } + } + } + } + ] +} +``` + +# Emitted Messages + +```block +The configuration biome.json has been successfully migrated. +``` diff --git a/crates/biome_json_syntax/src/member_ext.rs b/crates/biome_json_syntax/src/member_ext.rs index 1fbf57a8aaed..6f7973b740ae 100644 --- a/crates/biome_json_syntax/src/member_ext.rs +++ b/crates/biome_json_syntax/src/member_ext.rs @@ -1,8 +1,23 @@ -use crate::{inner_string_text, JsonMemberName}; -use biome_rowan::{SyntaxResult, TokenText}; +use crate::{inner_string_text, AnyJsonValue, JsonMember, JsonMemberName, JsonSyntaxToken}; +use biome_rowan::{AstSeparatedList, SyntaxResult, TokenText}; impl JsonMemberName { pub fn inner_string_text(&self) -> SyntaxResult { Ok(inner_string_text(&self.value_token()?)) } } + +impl JsonMember { + /// If the value of the member is a [JsonObjectValue](crate::JsonObjectValue), it returns a tuple + /// that contains a list of [JsonMember] and of its separators. Returns [None] otherwise + pub fn unzip_elements(&self) -> Option<(Vec, Vec)> { + let value = self.value().ok()?; + let AnyJsonValue::JsonObjectValue(value) = value else { + return None; + }; + + let members = value.json_member_list().iter().flatten().collect(); + let separators = value.json_member_list().separators().flatten().collect(); + Some((members, separators)) + } +} diff --git a/crates/biome_migrate/src/analyzers.rs b/crates/biome_migrate/src/analyzers.rs index c4f154cd2a8f..89af0c9a526b 100644 --- a/crates/biome_migrate/src/analyzers.rs +++ b/crates/biome_migrate/src/analyzers.rs @@ -6,6 +6,7 @@ use crate::analyzers::nursery_rules::NurseryRules; use crate::analyzers::organize_imports::OrganizeImports; use crate::analyzers::schema::Schema; use crate::analyzers::style_rules::StyleRules; +use crate::analyzers::trailing_comma::TrailingComma; use crate::analyzers::use_while::UseWhile; use biome_analyze::{GroupCategory, RegistryVisitor, RuleCategory, RuleGroup}; use biome_json_syntax::JsonLanguage; @@ -18,6 +19,7 @@ mod nursery_rules; mod organize_imports; mod schema; mod style_rules; +mod trailing_comma; mod use_while; pub(crate) struct MigrationGroup; @@ -42,6 +44,7 @@ impl RuleGroup for MigrationGroup { registry.record_rule::(); registry.record_rule::(); registry.record_rule::(); + registry.record_rule::(); } } diff --git a/crates/biome_migrate/src/analyzers/no_var.rs b/crates/biome_migrate/src/analyzers/no_var.rs index 3eb1651938ce..1a53199dd671 100644 --- a/crates/biome_migrate/src/analyzers/no_var.rs +++ b/crates/biome_migrate/src/analyzers/no_var.rs @@ -1,11 +1,13 @@ -use crate::rule_mover::AnalyzerMover; use crate::{declare_migration, MigrationAction}; use biome_analyze::context::RuleContext; use biome_analyze::{Ast, Rule, RuleAction, RuleDiagnostic}; use biome_console::markup; use biome_diagnostics::{category, Applicability}; -use biome_json_syntax::JsonMember; -use biome_rowan::{AstNode, TextRange}; +use biome_json_factory::make::{ + json_member, json_member_list, json_member_name, json_object_value, json_string_literal, token, +}; +use biome_json_syntax::{AnyJsonValue, JsonMember, T}; +use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt, TriviaPieceKind}; declare_migration! { pub(crate) NoVar { @@ -16,7 +18,7 @@ declare_migration! { impl Rule for NoVar { type Query = Ast; - type State = TextRange; + type State = JsonMember; type Signals = Option; type Options = (); @@ -26,25 +28,26 @@ impl Rule for NoVar { let name = node.name().ok()?; let text = name.inner_string_text().ok()?; + let mut no_var_member = None; if text.text() == "style" { - if let Some(object) = node.value().ok()?.as_json_object_value() { - for item in object.json_member_list().into_iter().flatten() { - let name = item.name().ok()?; - let text = name.inner_string_text().ok()?; - if text.text() == "noVar" { - return Some(name.range()); - } + let object = node.value().ok()?; + let object = object.as_json_object_value()?; + for item in object.json_member_list().into_iter().flatten() { + let name = item.name().ok()?; + let text = name.inner_string_text().ok()?; + if text.text() == "noVar" { + no_var_member = Some(item); } } } - None + no_var_member } fn diagnostic(_ctx: &RuleContext, state: &Self::State) -> Option { Some(RuleDiagnostic::new( category!("migrate"), - state, + state.range(), markup! { "The rule ""style/noVar"" has ben moved to the ""suspicious"" group." } @@ -52,13 +55,140 @@ impl Rule for NoVar { )) } - fn action(ctx: &RuleContext, _state: &Self::State) -> Option { - let root = ctx.root(); - let mut rule_mover = AnalyzerMover::from_root(root.clone()); - rule_mover.move_rule("noVar", "style", "suspicious"); - let mutation = rule_mover.run_queries()?; + fn action(ctx: &RuleContext, state: &Self::State) -> Option { + let mut mutation = ctx.root().begin(); + let no_var_member = state; + let style_member = ctx.query(); + + let style_member_list = style_member + .value() + .ok()? + .as_json_object_value()? + .json_member_list() + .iter() + .flatten() + .filter_map(|member| { + let name = member.name().ok()?; + let text = name.inner_string_text().ok()?; + if text.text() == "noVar" { + None + } else { + Some(member) + } + }) + .collect::>(); + + let mut separators = vec![]; + for _ in 0..style_member_list.len().saturating_sub(1) { + separators.push(token(T![,])) + } + + let rules_member = style_member.syntax().ancestors().find_map(|node| { + let member = JsonMember::cast(node)?; + let name = member.name().ok()?; + let text = name.inner_string_text().ok()?; + if text.text() == "rules" { + Some(member) + } else { + None + } + })?; + + let style_member = json_member( + style_member.name().ok()?, + style_member.colon_token().ok()?, + AnyJsonValue::JsonObjectValue(json_object_value( + token(T!['{']).with_leading_trivia(vec![(TriviaPieceKind::Whitespace, " ")]), + json_member_list(style_member_list, separators), + token(T!['}']).with_leading_trivia(vec![ + (TriviaPieceKind::Newline, "\n"), + (TriviaPieceKind::Whitespace, " ".repeat(6).as_str()), + ]), + )), + ); + + let linter_member_list = rules_member + .value() + .ok()? + .as_json_object_value()? + .json_member_list(); + + let suspicious_member = linter_member_list + .iter() + .flatten() + .find_map(|member| { + let name = member.name().ok()?.inner_string_text().ok()?; + if name.text() == "suspicious" { + Some(member) + } else { + None + } + }) + .and_then(|suspicious_member| { + // we inject the new member here + let (list, mut separators) = suspicious_member.unzip_elements()?; + for _ in 0..list.len().saturating_sub(1) { + separators.push(token(T![,])) + } + + Some(json_member( + suspicious_member.name().ok()?, + suspicious_member.colon_token().ok()?, + AnyJsonValue::JsonObjectValue(json_object_value( + suspicious_member + .value() + .ok()? + .as_json_object_value()? + .l_curly_token() + .ok()?, + json_member_list(list, separators), + suspicious_member + .value() + .ok()? + .as_json_object_value()? + .r_curly_token() + .ok()?, + )), + )) + }) + .unwrap_or(json_member( + json_member_name(json_string_literal("suspicious").with_leading_trivia(vec![ + (TriviaPieceKind::Newline, "\n"), + (TriviaPieceKind::Whitespace, " ".repeat(4).as_str()), + ])), + token(T![:]).with_trailing_trivia(vec![(TriviaPieceKind::Whitespace, " ")]), + AnyJsonValue::JsonObjectValue(json_object_value( + token(T!['{']).with_leading_trivia(vec![(TriviaPieceKind::Whitespace, " ")]), + json_member_list(vec![no_var_member.clone()], vec![]), + token(T!['}']).with_leading_trivia(vec![ + (TriviaPieceKind::Newline, "\n"), + (TriviaPieceKind::Whitespace, " ".repeat(4).as_str()), + ]), + )), + )); + + let mut separators = vec![]; + let mut new_members = vec![]; + for item in linter_member_list.iter().flatten() { + let name = item.name().ok()?.inner_string_text().ok()?; + if name.text() != "suspicious" { + new_members.push(suspicious_member.clone()); + } else if name.text() == "style" { + new_members.push(style_member.clone()); + } else { + new_members.push(item); + } + } + new_members.push(suspicious_member); + for _ in 0..new_members.len().saturating_sub(1) { + separators.push(token(T![,])) + } + + mutation.replace_node( + linter_member_list, + json_member_list(new_members, separators), + ); - // mutation.replace_node(state.clone(), new_list); Some(RuleAction::new( ctx.metadata().action_category(ctx.category(), ctx.group()), Applicability::Always, diff --git a/crates/biome_migrate/src/analyzers/organize_imports.rs b/crates/biome_migrate/src/analyzers/organize_imports.rs index a36a4775c7e0..fb2d2ee4a657 100644 --- a/crates/biome_migrate/src/analyzers/organize_imports.rs +++ b/crates/biome_migrate/src/analyzers/organize_imports.rs @@ -93,8 +93,9 @@ impl Rule for OrganizeImports { ]), )), ); - let assist_member = json_member( - json_member_name(json_string_literal("assist").with_leading_trivia(vec![ + + let actions_member = json_member( + json_member_name(json_string_literal("actions").with_leading_trivia(vec![ (TriviaPieceKind::Newline, "\n"), (TriviaPieceKind::Whitespace, " ".repeat(4).as_str()), ])), @@ -108,6 +109,21 @@ impl Rule for OrganizeImports { ]), )), ); + let assist_member = json_member( + json_member_name(json_string_literal("assist").with_leading_trivia(vec![ + (TriviaPieceKind::Newline, "\n"), + (TriviaPieceKind::Whitespace, " ".repeat(2).as_str()), + ])), + token(T![:]).with_trailing_trivia(vec![(TriviaPieceKind::Whitespace, " ")]), + AnyJsonValue::JsonObjectValue(json_object_value( + token(T!['{']).with_leading_trivia(vec![(TriviaPieceKind::Whitespace, " ")]), + json_member_list(vec![actions_member], vec![]), + token(T!['}']).with_leading_trivia(vec![ + (TriviaPieceKind::Newline, "\n"), + (TriviaPieceKind::Whitespace, " ".repeat(2).as_str()), + ]), + )), + ); mutation.replace_node(query.clone(), assist_member); diff --git a/crates/biome_migrate/src/analyzers/trailing_comma.rs b/crates/biome_migrate/src/analyzers/trailing_comma.rs new file mode 100644 index 000000000000..4b945bc42721 --- /dev/null +++ b/crates/biome_migrate/src/analyzers/trailing_comma.rs @@ -0,0 +1,61 @@ +use crate::{declare_migration, MigrationAction}; +use biome_analyze::context::RuleContext; +use biome_analyze::{Ast, Rule, RuleAction, RuleDiagnostic}; +use biome_console::markup; +use biome_diagnostics::{category, Applicability}; +use biome_json_factory::make::{json_member_name, json_string_literal}; +use biome_json_syntax::{JsonMember, JsonMemberName}; +use biome_rowan::{AstNode, BatchMutationExt}; + +declare_migration! { + pub(crate) TrailingComma { + version: "2.0.0", + name: "trailingComma", + } +} + +impl Rule for TrailingComma { + type Query = Ast; + type State = JsonMemberName; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + + let name = node.name().ok()?; + let node_text = name.inner_string_text().ok()?; + if node_text.text() == "trailingComma" { + return Some(name); + } + + None + } + + fn diagnostic(_ctx: &RuleContext, state: &Self::State) -> Option { + Some(RuleDiagnostic::new( + category!("migrate"), + state.range(), + markup! { + "The option trailingComma is removed. " + } + .to_owned(), + )) + } + + fn action(ctx: &RuleContext, state: &Self::State) -> Option { + let mut mutation = ctx.root().begin(); + let new_name = json_member_name(json_string_literal("trailingCommas")); + mutation.replace_node(state.clone(), new_name); + + Some(RuleAction::new( + ctx.metadata().action_category(ctx.category(), ctx.group()), + Applicability::Always, + markup! { + "Use the option trailingCommas instead." + } + .to_owned(), + mutation, + )) + } +} diff --git a/crates/biome_migrate/tests/specs/migrations/noVar/invalid.json.snap b/crates/biome_migrate/tests/specs/migrations/noVar/invalid.json.snap index b44f74b59609..e699630039b3 100644 --- a/crates/biome_migrate/tests/specs/migrations/noVar/invalid.json.snap +++ b/crates/biome_migrate/tests/specs/migrations/noVar/invalid.json.snap @@ -26,7 +26,7 @@ invalid.json:5:9 migrate FIXABLE ━━━━━━━━━━━━━━━ 3 │ "rules": { 4 │ "style": { > 5 │ "noVar": "error" - │ ^^^^^^^ + │ ^^^^^^^^^^^^^^^^ 6 │ } 7 │ } @@ -35,9 +35,15 @@ invalid.json:5:9 migrate FIXABLE ━━━━━━━━━━━━━━━ 2 2 │ "linter": { 3 3 │ "rules": { 4 │ - ······"style":·{ - 4 │ + ······"suspicious":·{ - 5 5 │ "noVar": "error" - 6 6 │ } + 4 │ + ······"suspicious":··{ + 5 │ + ········"noVar":·"error" + 6 │ + ····}, + 7 │ + ····"suspicious":··{ + 5 8 │ "noVar": "error" + 6 │ - ······} + 9 │ + ····} + 7 10 │ } + 8 11 │ } ``` diff --git a/crates/biome_migrate/tests/specs/migrations/noVar/invalid_with_group.json b/crates/biome_migrate/tests/specs/migrations/noVar/invalid_with_group.json new file mode 100644 index 000000000000..46a350d75aec --- /dev/null +++ b/crates/biome_migrate/tests/specs/migrations/noVar/invalid_with_group.json @@ -0,0 +1,13 @@ +{ + "linter": { + "rules": { + "suspicious": { + "foo": "bar" + }, + "style": { + "lorem": "ipsum", + "noVar": "error" + } + } + } +} diff --git a/crates/biome_migrate/tests/specs/migrations/noVar/invalid_with_group.json.snap b/crates/biome_migrate/tests/specs/migrations/noVar/invalid_with_group.json.snap new file mode 100644 index 000000000000..b59b6befbeec --- /dev/null +++ b/crates/biome_migrate/tests/specs/migrations/noVar/invalid_with_group.json.snap @@ -0,0 +1,53 @@ +--- +source: crates/biome_migrate/tests/spec_tests.rs +expression: invalid_with_group.json +snapshot_kind: text +--- +# Input +```json +{ + "linter": { + "rules": { + "suspicious": { + "foo": "bar" + }, + "style": { + "lorem": "ipsum", + "noVar": "error" + } + } + } +} + +``` + +# Diagnostics +``` +invalid_with_group.json:9:9 migrate FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The rule style/noVar has ben moved to the suspicious group. + + 7 │ "style": { + 8 │ "lorem": "ipsum", + > 9 │ "noVar": "error" + │ ^^^^^^^^^^^^^^^^ + 10 │ } + 11 │ } + + i Safe fix: Move the rule in the correct group. + + 5 5 │ "foo": "bar" + 6 6 │ }, + 7 │ - ······"style":·{ + 8 │ - ········"lorem":·"ipsum", + 9 │ - ········"noVar":·"error" + 7 │ + ······"suspicious":·{ + 8 │ + ········"foo":·"bar" + 9 │ + ······}, + 10 │ + ······"suspicious":·{ + 11 │ + ········"foo":·"bar" + 10 12 │ } + 11 13 │ } + + +``` diff --git a/crates/biome_migrate/tests/specs/migrations/organizeImports/invalid.json.snap b/crates/biome_migrate/tests/specs/migrations/organizeImports/invalid.json.snap index 1ed91669c26f..51a985949d6a 100644 --- a/crates/biome_migrate/tests/specs/migrations/organizeImports/invalid.json.snap +++ b/crates/biome_migrate/tests/specs/migrations/organizeImports/invalid.json.snap @@ -30,17 +30,17 @@ invalid.json:3:5 migrate FIXABLE ━━━━━━━━━━━━━━━ i Safe fix: Remove the old configuration, and turn off the relative assist action. - 1 1 │ { - 2 │ - ··"organizeImports":·{ - 3 │ - ····"enabled":·false - 4 │ - ··} - 2 │ + ··"assist":··{ - 3 │ + ······"source":··{ - 4 │ + ········"organizeImports":·"off" - 5 │ + ······} - 6 │ + ····} - 5 7 │ } - 6 8 │ + 1 1 │ { + 2 │ - ··"organizeImports":·{ + 3 │ - ····"enabled":·false + 2 │ + ··"assist":··{ + 3 │ + ····"actions":··{ + 4 │ + ······"source":··{ + 5 │ + ········"organizeImports":·"off" + 6 │ + ······} + 7 │ + ····} + 4 8 │ } + 5 9 │ } ``` diff --git a/crates/biome_migrate/tests/specs/migrations/trailingComma/invalid.json b/crates/biome_migrate/tests/specs/migrations/trailingComma/invalid.json new file mode 100644 index 000000000000..2719317f96e8 --- /dev/null +++ b/crates/biome_migrate/tests/specs/migrations/trailingComma/invalid.json @@ -0,0 +1,12 @@ +{ + "javascript": { + "formatter": { + "trailingComma": "always" + } + }, + "json": { + "formatter": { + "trailingComma": "always" + } + } +} diff --git a/crates/biome_migrate/tests/specs/migrations/trailingComma/invalid.json.snap b/crates/biome_migrate/tests/specs/migrations/trailingComma/invalid.json.snap new file mode 100644 index 000000000000..497e6f5d3c23 --- /dev/null +++ b/crates/biome_migrate/tests/specs/migrations/trailingComma/invalid.json.snap @@ -0,0 +1,70 @@ +--- +source: crates/biome_migrate/tests/spec_tests.rs +expression: invalid.json +snapshot_kind: text +--- +# Input +```json +{ + "javascript": { + "formatter": { + "trailingComma": "always" + } + }, + "json": { + "formatter": { + "trailingComma": "always" + } + } +} + +``` + +# Diagnostics +``` +invalid.json:4:7 migrate FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The option trailingComma is removed. + + 2 │ "javascript": { + 3 │ "formatter": { + > 4 │ "trailingComma": "always" + │ ^^^^^^^^^^^^^^^ + 5 │ } + 6 │ }, + + i Safe fix: Use the option trailingCommas instead. + + 2 2 │ "javascript": { + 3 3 │ "formatter": { + 4 │ - ······"trailingComma":·"always" + 4 │ + ······"trailingCommas":·"always" + 5 5 │ } + 6 6 │ }, + + +``` + +``` +invalid.json:9:7 migrate FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The option trailingComma is removed. + + 7 │ "json": { + 8 │ "formatter": { + > 9 │ "trailingComma": "always" + │ ^^^^^^^^^^^^^^^ + 10 │ } + 11 │ } + + i Safe fix: Use the option trailingCommas instead. + + 7 7 │ "json": { + 8 8 │ "formatter": { + 9 │ - ······"trailingComma":·"always" + 9 │ + ······"trailingCommas":·"always" + 10 10 │ } + 11 11 │ } + + +```