diff --git a/Cargo.lock b/Cargo.lock index 846fa2f6fd6..cefdc82e2ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -706,12 +706,14 @@ dependencies = [ "biome_analyze", "biome_console", "biome_diagnostics", + "biome_json_factory", "biome_json_parser", "biome_json_syntax", "biome_rowan", "biome_test_utils", "insta", "lazy_static", + "natord", "rustc-hash", "tests_macros", ] diff --git a/Cargo.toml b/Cargo.toml index e80c8c89ee8..ee583de14a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,6 +160,7 @@ ignore = "0.4.21" indexmap = "1.9.3" insta = "1.36.1" lazy_static = "1.4.0" +natord = "1.0.9" oxc_resolver = "1.4.0" quickcheck = "1.0.3" quickcheck_macros = "1.0.0" @@ -175,7 +176,6 @@ tokio = { version = "1.36.0" } tracing = { version = "0.1.37", default-features = false, features = ["std"] } unicode-bom = "2.0.3" - [profile.dev.package.biome_wasm] debug = true opt-level = "s" diff --git a/crates/biome_analyze/src/rule.rs b/crates/biome_analyze/src/rule.rs index 461a7cec210..ca6ffcb3c5b 100644 --- a/crates/biome_analyze/src/rule.rs +++ b/crates/biome_analyze/src/rule.rs @@ -8,11 +8,11 @@ use biome_console::fmt::Display; use biome_console::{markup, MarkupBuf}; use biome_diagnostics::advice::CodeSuggestionAdvice; use biome_diagnostics::location::AsSpan; -use biome_diagnostics::Applicability; use biome_diagnostics::{ Advices, Category, Diagnostic, DiagnosticTags, Location, LogCategory, MessageAndDescription, Visit, }; +use biome_diagnostics::{Applicability, DiagnosticExt}; use biome_rowan::{AstNode, BatchMutation, BatchMutationExt, Language, TextRange}; use std::cmp::Ordering; use std::fmt::Debug; @@ -347,7 +347,7 @@ pub trait RuleGroup { /// This macro is used by the codegen script to declare an analyzer rule group, /// and implement the [RuleGroup] trait for it #[macro_export] -macro_rules! declare_group { +macro_rules! declare_lint_group { ( $vis:vis $id:ident { name: $name:tt, rules: [ $( $( $rule:ident )::* , )* ] } ) => { $vis enum $id {} @@ -381,6 +381,43 @@ macro_rules! declare_group { }; } +/// This macro is used by the codegen script to declare an analyzer rule group, +/// and implement the [RuleGroup] trait for it +#[macro_export] +macro_rules! declare_assists_group { + ( $vis:vis $id:ident { name: $name:tt, rules: [ $( $( $rule:ident )::* , )* ] } ) => { + $vis enum $id {} + + impl $crate::RuleGroup for $id { + type Language = <( $( $( $rule )::* , )* ) as $crate::GroupLanguage>::Language; + type Category = super::Category; + + const NAME: &'static str = $name; + + fn record_rules + ?Sized>(registry: &mut V) { + $( registry.record_rule::<$( $rule )::*>(); )* + } + } + + pub(self) use $id as Group; + + // Declare a `group_category!` macro in the context of this module (and + // all its children). This macro takes the name of a rule as a string + // literal token and expands to the category of the lint rule with this + // name within this group. + // This is implemented by calling the `category_concat!` macro with the + // "lint" prefix, the name of this group, and the rule name argument + #[allow(unused_macros)] + macro_rules! group_category { + ( $rule_name:tt ) => { $crate::category_concat!( "assists", $name, $rule_name ) }; + } + + // Re-export the macro for child modules, so `declare_rule!` can access + // the category of its parent group by using the `super` module + pub(self) use group_category; + }; +} + /// A group category is a collection of rule groups under a given category ID, /// serving as a broad classification on the kind of diagnostic or code action /// these rule emit, and allowing whole categories of rules to be disabled at diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index 75e80277d7f..72e12941a31 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_analyze/src/lint/nursery.rs @@ -1,11 +1,11 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_color_invalid_hex; pub mod no_duplicate_font_names; -declare_group! { +declare_lint_group! { pub Nursery { name : "nursery" , rules : [ diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 8021c4aab59..cc48376ecca 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -113,13 +113,13 @@ define_categories! { "lint/nursery/noConsole": "https://biomejs.dev/linter/rules/no-console", "lint/nursery/noDoneCallback": "https://biomejs.dev/linter/rules/no-done-callback", "lint/nursery/noDuplicateElseIf": "https://biomejs.dev/linter/rules/no-duplicate-else-if", + "lint/nursery/noDuplicateFontNames": "https://biomejs.dev/linter/rules/no-font-family-duplicate-names", "lint/nursery/noDuplicateJsonKeys": "https://biomejs.dev/linter/rules/no-duplicate-json-keys", "lint/nursery/noDuplicateTestHooks": "https://biomejs.dev/linter/rules/no-duplicate-test-hooks", "lint/nursery/noEvolvingAny": "https://biomejs.dev/linter/rules/no-evolving-any", "lint/nursery/noExcessiveNestedTestSuites": "https://biomejs.dev/linter/rules/no-excessive-nested-test-suites", "lint/nursery/noExportsInTest": "https://biomejs.dev/linter/rules/no-exports-in-test", "lint/nursery/noFocusedTests": "https://biomejs.dev/linter/rules/no-focused-tests", - "lint/nursery/noDuplicateFontNames": "https://biomejs.dev/linter/rules/no-font-family-duplicate-names", "lint/nursery/noMisplacedAssertion": "https://biomejs.dev/linter/rules/no-misplaced-assertion", "lint/nursery/noNamespaceImport": "https://biomejs.dev/linter/rules/no-namespace-import", "lint/nursery/noNodejsModules": "https://biomejs.dev/linter/rules/no-nodejs-modules", @@ -231,6 +231,7 @@ define_categories! { "lint/suspicious/useIsArray": "https://biomejs.dev/linter/rules/use-is-array", "lint/suspicious/useNamespaceKeyword": "https://biomejs.dev/linter/rules/use-namespace-keyword", "lint/suspicious/useValidTypeof": "https://biomejs.dev/linter/rules/use-valid-typeof", + "assists/nursery/useSortedKeys": "https://biomejs.dev/linter/rules/use-sorted-keys", ; // General categories "files/missingHandler", diff --git a/crates/biome_js_analyze/Cargo.toml b/crates/biome_js_analyze/Cargo.toml index 3fac7aed0d7..ba7b626bc75 100644 --- a/crates/biome_js_analyze/Cargo.toml +++ b/crates/biome_js_analyze/Cargo.toml @@ -27,7 +27,7 @@ biome_string_case = { workspace = true } biome_suppression = { workspace = true } biome_unicode_table = { workspace = true } lazy_static = { workspace = true } -natord = "1.0.9" +natord = { workspace = true } roaring = "0.10.1" rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } diff --git a/crates/biome_js_analyze/src/assists/correctness.rs b/crates/biome_js_analyze/src/assists/correctness.rs index 4fe923edace..4929a52b156 100644 --- a/crates/biome_js_analyze/src/assists/correctness.rs +++ b/crates/biome_js_analyze/src/assists/correctness.rs @@ -1,10 +1,10 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_assists_group; pub mod organize_imports; -declare_group! { +declare_assists_group! { pub Correctness { name : "correctness" , rules : [ diff --git a/crates/biome_js_analyze/src/lint/a11y.rs b/crates/biome_js_analyze/src/lint/a11y.rs index 57cfc1baf88..83fc66360d5 100644 --- a/crates/biome_js_analyze/src/lint/a11y.rs +++ b/crates/biome_js_analyze/src/lint/a11y.rs @@ -1,6 +1,6 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_access_key; pub mod no_aria_hidden_on_focusable; @@ -33,7 +33,7 @@ pub mod use_valid_aria_role; pub mod use_valid_aria_values; pub mod use_valid_lang; -declare_group! { +declare_lint_group! { pub A11y { name : "a11y" , rules : [ diff --git a/crates/biome_js_analyze/src/lint/complexity.rs b/crates/biome_js_analyze/src/lint/complexity.rs index 35bd089a143..093d762ffdd 100644 --- a/crates/biome_js_analyze/src/lint/complexity.rs +++ b/crates/biome_js_analyze/src/lint/complexity.rs @@ -1,6 +1,6 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_banned_types; pub mod no_empty_type_parameters; @@ -30,7 +30,7 @@ pub mod use_regex_literals; pub mod use_simple_number_keys; pub mod use_simplified_logic_expression; -declare_group! { +declare_lint_group! { pub Complexity { name : "complexity" , rules : [ diff --git a/crates/biome_js_analyze/src/lint/correctness.rs b/crates/biome_js_analyze/src/lint/correctness.rs index c19fa405bb2..c0e660df917 100644 --- a/crates/biome_js_analyze/src/lint/correctness.rs +++ b/crates/biome_js_analyze/src/lint/correctness.rs @@ -1,6 +1,6 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_children_prop; pub mod no_const_assign; @@ -39,7 +39,7 @@ pub mod use_is_nan; pub mod use_valid_for_direction; pub mod use_yield; -declare_group! { +declare_lint_group! { pub Correctness { name : "correctness" , rules : [ diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index f1c673533ec..3e883e2b31d 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -1,6 +1,6 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_barrel_file; pub mod no_console; @@ -25,7 +25,7 @@ pub mod use_jsx_key_in_iterable; pub mod use_node_assert_strict; pub mod use_sorted_classes; -declare_group! { +declare_lint_group! { pub Nursery { name : "nursery" , rules : [ diff --git a/crates/biome_js_analyze/src/lint/performance.rs b/crates/biome_js_analyze/src/lint/performance.rs index 3220ca9597f..0b94a6281f7 100644 --- a/crates/biome_js_analyze/src/lint/performance.rs +++ b/crates/biome_js_analyze/src/lint/performance.rs @@ -1,11 +1,11 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_accumulating_spread; pub mod no_delete; -declare_group! { +declare_lint_group! { pub Performance { name : "performance" , rules : [ diff --git a/crates/biome_js_analyze/src/lint/security.rs b/crates/biome_js_analyze/src/lint/security.rs index 75abfba1459..309d737734a 100644 --- a/crates/biome_js_analyze/src/lint/security.rs +++ b/crates/biome_js_analyze/src/lint/security.rs @@ -1,12 +1,12 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_dangerously_set_inner_html; pub mod no_dangerously_set_inner_html_with_children; pub mod no_global_eval; -declare_group! { +declare_lint_group! { pub Security { name : "security" , rules : [ diff --git a/crates/biome_js_analyze/src/lint/style.rs b/crates/biome_js_analyze/src/lint/style.rs index e279fbb79e8..f004c54e67b 100644 --- a/crates/biome_js_analyze/src/lint/style.rs +++ b/crates/biome_js_analyze/src/lint/style.rs @@ -1,6 +1,6 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_arguments; pub mod no_comma_operator; @@ -44,7 +44,7 @@ pub mod use_single_var_declarator; pub mod use_template; pub mod use_while; -declare_group! { +declare_lint_group! { pub Style { name : "style" , rules : [ diff --git a/crates/biome_js_analyze/src/lint/suspicious.rs b/crates/biome_js_analyze/src/lint/suspicious.rs index e96b2caee6e..44242369cac 100644 --- a/crates/biome_js_analyze/src/lint/suspicious.rs +++ b/crates/biome_js_analyze/src/lint/suspicious.rs @@ -1,6 +1,6 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_approximative_numeric_constant; pub mod no_array_index_key; @@ -53,7 +53,7 @@ pub mod use_is_array; pub mod use_namespace_keyword; pub mod use_valid_typeof; -declare_group! { +declare_lint_group! { pub Suspicious { name : "suspicious" , rules : [ diff --git a/crates/biome_js_analyze/src/syntax/correctness.rs b/crates/biome_js_analyze/src/syntax/correctness.rs index 18f9631b4d6..641fc626bd4 100644 --- a/crates/biome_js_analyze/src/syntax/correctness.rs +++ b/crates/biome_js_analyze/src/syntax/correctness.rs @@ -1,12 +1,12 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_duplicate_private_class_members; pub mod no_initializer_with_definite; pub mod no_super_without_extends; -declare_group! { +declare_lint_group! { pub Correctness { name : "correctness" , rules : [ diff --git a/crates/biome_js_analyze/src/syntax/nursery.rs b/crates/biome_js_analyze/src/syntax/nursery.rs index 00faddee221..fa59ba13e3f 100644 --- a/crates/biome_js_analyze/src/syntax/nursery.rs +++ b/crates/biome_js_analyze/src/syntax/nursery.rs @@ -1,10 +1,10 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_type_only_import_attributes; -declare_group! { +declare_lint_group! { pub Nursery { name : "nursery" , rules : [ diff --git a/crates/biome_json_analyze/Cargo.toml b/crates/biome_json_analyze/Cargo.toml index a645de57ec1..7e818dbb375 100644 --- a/crates/biome_json_analyze/Cargo.toml +++ b/crates/biome_json_analyze/Cargo.toml @@ -13,13 +13,15 @@ version = "0.5.7" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -biome_analyze = { workspace = true } -biome_console = { workspace = true } -biome_diagnostics = { workspace = true } -biome_json_syntax = { workspace = true } -biome_rowan = { workspace = true } -lazy_static = { workspace = true } -rustc-hash = { 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 } +natord = { workspace = true } +rustc-hash = { workspace = true } [dev-dependencies] biome_json_parser = { path = "../biome_json_parser" } diff --git a/crates/biome_json_analyze/src/assists.rs b/crates/biome_json_analyze/src/assists.rs new file mode 100644 index 00000000000..120fa61115f --- /dev/null +++ b/crates/biome_json_analyze/src/assists.rs @@ -0,0 +1,4 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +pub mod nursery; +::biome_analyze::declare_category! { pub Assists { kind : Action , groups : [self :: nursery :: Nursery ,] } } diff --git a/crates/biome_json_analyze/src/assists/nursery.rs b/crates/biome_json_analyze/src/assists/nursery.rs new file mode 100644 index 00000000000..993ff89ebc6 --- /dev/null +++ b/crates/biome_json_analyze/src/assists/nursery.rs @@ -0,0 +1,14 @@ +//! Generated file, do not edit by hand, see `xtask/codegen` + +use biome_analyze::declare_assists_group; + +pub mod use_sorted_keys; + +declare_assists_group! { + pub Nursery { + name : "nursery" , + rules : [ + self :: use_sorted_keys :: UseSortedKeys , + ] + } +} diff --git a/crates/biome_json_analyze/src/assists/nursery/use_sorted_keys.rs b/crates/biome_json_analyze/src/assists/nursery/use_sorted_keys.rs new file mode 100644 index 00000000000..60ffb40a5dc --- /dev/null +++ b/crates/biome_json_analyze/src/assists/nursery/use_sorted_keys.rs @@ -0,0 +1,176 @@ +use crate::JsonRuleAction; +use biome_analyze::{ + context::RuleContext, declare_rule, ActionCategory, Ast, RefactorKind, Rule, RuleAction, + RuleDiagnostic, SourceActionKind, +}; +use biome_console::markup; +use biome_diagnostics::Applicability; +use biome_json_factory::make::{json_member_list, token}; +use biome_json_syntax::{JsonMember, JsonMemberList, JsonMemberName, JsonRoot, JsonSyntaxToken, T}; +use biome_rowan::{AstNode, AstNodeExt, AstSeparatedList, BatchMutationExt, TextRange, TextSize}; +use std::cmp::Ordering; +use std::collections::{BTreeMap, BTreeSet}; + +declare_rule! { + /// Succinct description of the rule. + /// + /// Put context and details about the rule. + /// As a starting point, you can take the description of the corresponding _ESLint_ rule (if any). + /// + /// Try to stay consistent with the descriptions of implemented rules. + /// + /// Add a link to the corresponding stylelint rule (if any): + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// p {} + /// ``` + /// + /// ### Valid + /// + /// ```css + /// p { + /// color: red; + /// } + /// ``` + /// + pub UseSortedKeys { + version: "next", + name: "useSortedKeys", + recommended: false, + } +} + +#[derive(Eq, PartialEq)] +pub struct MemberKey { + node: JsonMember, +} + +impl MemberKey { + fn range(&self) -> TextRange { + self.node.range() + } +} + +impl Ord for MemberKey { + fn cmp(&self, other: &Self) -> Ordering { + // Sort imports using natural ordering + natord::compare( + &self.node.name().unwrap().text(), + &other.node.name().unwrap().text(), + ) + } +} + +impl PartialOrd for MemberKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +pub struct Members(pub BTreeSet); + +impl Members { + /// Returns true if the nodes in the group are already sorted in the file + fn is_sorted(&self) -> bool { + // The imports are sorted if the text position of each node in the `BTreeMap` + // (sorted in natural order) is higher than the previous item in + // the sequence + let mut iter = self + .0 + .iter() + .map(|node| node.node.syntax().text_range().start()); + let mut previous_start = iter.next().unwrap_or_default(); + iter.all(|start| { + let is_sorted = previous_start < start; + previous_start = start; + is_sorted + }) + } + + fn to_sorted_node(&self) -> JsonMemberList { + let items = self.0.iter().map(|key| key.node.clone().detach()); + + let separator_count = items.len().saturating_sub(1); + + let mut separators = Vec::new(); + + for (index, _) in self.0.iter().enumerate() { + if index == separator_count { + continue; + } else { + separators.push(token(T![,])) + } + } + + let member_list = json_member_list(items, separators); + + member_list + } +} + +impl Rule for UseSortedKeys { + type Query = Ast; + type State = Members; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Option { + let node = ctx.query(); + + if node.is_empty() { + return None; + } + + let state = node + .iter() + .filter_map(|node| { + let node = node.ok()?; + Some(MemberKey { node }) + }) + .collect::>(); + + let state = Members(state); + + if !state.is_sorted() { + Some(state) + } else { + None + } + } + + fn diagnostic(ctx: &RuleContext, node: &Self::State) -> Option { + let node = ctx.query(); + // + // Read our guidelines to write great diagnostics: + // https://docs.rs/biome_analyze/latest/biome_analyze/#what-a-rule-should-say-to-the-user + // + Some(RuleDiagnostic::new( + rule_category!(), + node.range(), + markup! { + "The key can be sorted" + }, + )) + } + + fn action(ctx: &RuleContext, state: &Self::State) -> Option { + let list = state.to_sorted_node(); + let mut mutation = ctx.root().begin(); + let node = ctx.query().clone(); + mutation.replace_node(node, list); + + Some(RuleAction { + mutation, + category: ActionCategory::Refactor(RefactorKind::Rewrite), + applicability: Applicability::MaybeIncorrect, + message: markup! { + "Sort the keys" + } + .to_owned(), + }) + } +} diff --git a/crates/biome_json_analyze/src/lib.rs b/crates/biome_json_analyze/src/lib.rs index 2189e5bada2..c4693d959cf 100644 --- a/crates/biome_json_analyze/src/lib.rs +++ b/crates/biome_json_analyze/src/lib.rs @@ -1,15 +1,19 @@ +mod assists; mod lint; + pub mod options; mod registry; pub use crate::registry::visit_registry; use biome_analyze::{ AnalysisFilter, AnalyzerOptions, AnalyzerSignal, ControlFlow, LanguageRoot, MatchQueryParams, - MetadataRegistry, RuleRegistry, SuppressionDiagnostic, SuppressionKind, + MetadataRegistry, RuleAction, RuleRegistry, SuppressionDiagnostic, SuppressionKind, }; use biome_diagnostics::Error; use biome_json_syntax::JsonLanguage; +pub(crate) type JsonRuleAction = RuleAction; + /// Return the static [MetadataRegistry] for the JSON analyzer rules pub fn metadata() -> &'static MetadataRegistry { lazy_static::lazy_static! { diff --git a/crates/biome_json_analyze/src/lint/nursery.rs b/crates/biome_json_analyze/src/lint/nursery.rs index 1de83c6e36d..d64e3a78616 100644 --- a/crates/biome_json_analyze/src/lint/nursery.rs +++ b/crates/biome_json_analyze/src/lint/nursery.rs @@ -1,10 +1,10 @@ //! Generated file, do not edit by hand, see `xtask/codegen` -use biome_analyze::declare_group; +use biome_analyze::declare_lint_group; pub mod no_duplicate_json_keys; -declare_group! { +declare_lint_group! { pub Nursery { name : "nursery" , rules : [ diff --git a/crates/biome_json_analyze/src/registry.rs b/crates/biome_json_analyze/src/registry.rs index 5df3b0cfc10..a5f182e1e87 100644 --- a/crates/biome_json_analyze/src/registry.rs +++ b/crates/biome_json_analyze/src/registry.rs @@ -4,4 +4,5 @@ use biome_analyze::RegistryVisitor; use biome_json_syntax::JsonLanguage; pub fn visit_registry>(registry: &mut V) { registry.record_category::(); + registry.record_category::(); } diff --git a/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/invalid.json b/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/invalid.json new file mode 100644 index 00000000000..202d76e1ddf --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/invalid.json @@ -0,0 +1,4 @@ +{ + "zed": "", + "alpha": "fff" +} diff --git a/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/invalid.json.snap b/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/invalid.json.snap new file mode 100644 index 00000000000..784549885f1 --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/invalid.json.snap @@ -0,0 +1,39 @@ +--- +source: crates/biome_json_analyze/tests/spec_tests.rs +expression: invalid.json +--- +# Input +```json +{ + "zed": "", + "alpha": "fff" +} + +``` + +# Diagnostics +``` +invalid.json:2:2 assists/nursery/useSortedKeys FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The key can be sorted + + 1 │ { + > 2 │ "zed": "", + │ ^^^^^^^^^^ + > 3 │ "alpha": "fff" + │ ^^^^^^^^^^^^^^ + 4 │ } + 5 │ + + i Unsafe fix: Sort the keys + + 1 1 │ { + 2 │ - → "zed":·"", + 3 │ - → "alpha":·"fff" + 2 │ + → "alpha":·"fff", + 3 │ + → "zed":·"" + 4 4 │ } + 5 5 │ + + +``` diff --git a/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/valid.json b/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/valid.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/valid.json @@ -0,0 +1 @@ +{} diff --git a/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/valid.json.snap b/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/valid.json.snap new file mode 100644 index 00000000000..e6306f075ce --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/valid.json.snap @@ -0,0 +1,9 @@ +--- +source: crates/biome_json_analyze/tests/spec_tests.rs +expression: valid.json +--- +# Input +```json +{} + +``` diff --git a/crates/biome_json_factory/src/make.rs b/crates/biome_json_factory/src/make.rs index fd5cb550cfe..cde739d5d7b 100644 --- a/crates/biome_json_factory/src/make.rs +++ b/crates/biome_json_factory/src/make.rs @@ -2,6 +2,14 @@ use biome_json_syntax::{JsonSyntaxKind, JsonSyntaxToken}; pub use crate::generated::node_factory::*; +pub fn token(kind: JsonSyntaxKind) -> JsonSyntaxToken { + if let Some(text) = kind.to_string() { + JsonSyntaxToken::new_detached(kind, text, [], []) + } else { + panic!("token kind {kind:?} cannot be transformed to text") + } +} + pub fn ident(text: &str) -> JsonSyntaxToken { JsonSyntaxToken::new_detached(JsonSyntaxKind::IDENT, text, [], []) } diff --git a/justfile b/justfile index d72c4b5b8b7..cd3677c24d3 100644 --- a/justfile +++ b/justfile @@ -66,13 +66,25 @@ documentation: # Creates a new lint rule in the given path, with the given name. Name has to be camel case. new-js-lintrule rulename: - cargo run -p xtask_codegen -- new-lintrule --kind=js --name={{rulename}} + cargo run -p xtask_codegen -- new-lintrule --kind=js --category=lint --name={{rulename}} + just gen-lint + just documentation + +# Creates a new lint rule in the given path, with the given name. Name has to be camel case. +new-js-assistrule rulename: + cargo run -p xtask_codegen -- new-lintrule --kind=js --category=assist --name={{rulename}} + just gen-lint + just documentation + + # Creates a new lint rule in the given path, with the given name. Name has to be camel case. +new-json-assistrule rulename: + cargo run -p xtask_codegen -- new-lintrule --kind=json --category=assist --name={{rulename}} just gen-lint just documentation # Creates a new css lint rule in the given path, with the given name. Name has to be camel case. new-css-lintrule rulename: - cargo run -p xtask_codegen -- new-lintrule --kind=css --name={{rulename}} + cargo run -p xtask_codegen -- new-lintrule --kind=css --category=lint --name={{rulename}} just gen-lint # Promotes a rule from the nursery group to a new group diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 06d53f2f9ac..c415970f38a 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1917,13 +1917,13 @@ export type Category = | "lint/nursery/noConsole" | "lint/nursery/noDoneCallback" | "lint/nursery/noDuplicateElseIf" + | "lint/nursery/noDuplicateFontNames" | "lint/nursery/noDuplicateJsonKeys" | "lint/nursery/noDuplicateTestHooks" | "lint/nursery/noEvolvingAny" | "lint/nursery/noExcessiveNestedTestSuites" | "lint/nursery/noExportsInTest" | "lint/nursery/noFocusedTests" - | "lint/nursery/noDuplicateFontNames" | "lint/nursery/noMisplacedAssertion" | "lint/nursery/noNamespaceImport" | "lint/nursery/noNodejsModules" @@ -1939,6 +1939,7 @@ export type Category = | "lint/nursery/useJsxKeyInIterable" | "lint/nursery/useNodeAssertStrict" | "lint/nursery/useSortedClasses" + | "lint/nursery/useSortedKeys" | "lint/performance/noAccumulatingSpread" | "lint/performance/noDelete" | "lint/security/noDangerouslySetInnerHtml" diff --git a/xtask/codegen/src/generate_analyzer.rs b/xtask/codegen/src/generate_analyzer.rs index 3f938a305e5..2d89541395a 100644 --- a/xtask/codegen/src/generate_analyzer.rs +++ b/xtask/codegen/src/generate_analyzer.rs @@ -35,8 +35,11 @@ fn generate_json_analyzer() -> Result<()> { let mut analyzers = BTreeMap::new(); generate_category("lint", &mut analyzers, &base_path)?; + let mut assists = BTreeMap::new(); + generate_category("assists", &mut assists, &base_path)?; + generate_options(&base_path)?; - update_json_registry_builder(analyzers) + update_json_registry_builder(analyzers, assists) } fn generate_css_analyzer() -> Result<()> { @@ -189,12 +192,29 @@ fn generate_group(category: &'static str, group: &str, base_path: &Path) -> Resu let nl = Punct::new('\n', Spacing::Alone); let sp = Punct::new(' ', Spacing::Joint); let sp4 = quote! { #sp #sp #sp #sp }; + dbg!(&category); + let (import_macro, use_macro) = match category { + "lint" | "syntax" => ( + quote!( + use biome_analyze::declare_lint_group + ), + quote!(declare_lint_group), + ), + "assists" => ( + quote!( + use biome_analyze::declare_assists_group + ), + quote!(declare_assists_group), + ), + + _ => panic!("Category not supported: {category}"), + }; let tokens = xtask::reformat(quote! { - use biome_analyze::declare_group; + #import_macro; #nl #nl #( #rule_imports )* #nl #nl - declare_group! { #nl + #use_macro! { #nl #sp4 pub #group_name { #nl #sp4 #sp4 name: #group, #nl #sp4 #sp4 rules: [ #nl @@ -236,10 +256,16 @@ fn update_js_registry_builder( Ok(()) } -fn update_json_registry_builder(analyzers: BTreeMap<&'static str, TokenStream>) -> Result<()> { +fn update_json_registry_builder( + analyzers: BTreeMap<&'static str, TokenStream>, + assists: BTreeMap<&'static str, TokenStream>, +) -> Result<()> { let path = project_root().join("crates/biome_json_analyze/src/registry.rs"); - let categories = analyzers.into_values(); + let categories = analyzers + .into_iter() + .chain(assists) + .map(|(_, tokens)| tokens); let tokens = xtask::reformat(quote! { use biome_analyze::RegistryVisitor; diff --git a/xtask/codegen/src/generate_new_lintrule.rs b/xtask/codegen/src/generate_new_analyzer_rule.rs similarity index 74% rename from xtask/codegen/src/generate_new_lintrule.rs rename to xtask/codegen/src/generate_new_analyzer_rule.rs index 0946dae3cc4..2b2999c90a8 100644 --- a/xtask/codegen/src/generate_new_lintrule.rs +++ b/xtask/codegen/src/generate_new_analyzer_rule.rs @@ -33,6 +33,26 @@ impl FromStr for RuleKind { } } +#[derive(Debug, Clone, Bpaf)] +pub enum Category { + /// Lint rules + Lint, + /// Assist rules + Assist, +} + +impl FromStr for Category { + type Err = &'static str; + + fn from_str(s: &str) -> std::result::Result { + match s { + "lint" => Ok(Self::Lint), + "assist" => Ok(Self::Assist), + _ => Err("Not supported"), + } + } +} + fn generate_rule_template( kind: &RuleKind, rule_name_upper_camel: &str, @@ -192,15 +212,89 @@ impl Rule for {rule_name_upper_camel} {{ ) } RuleKind::Json => { - unimplemented!("JSON variant not implemented yet.") + format!( + r#"use biome_analyze::{{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic}}; +use biome_console::markup; +use biome_json_syntax::JsonRoot; +use biome_rowan::AstNode; + +declare_rule! {{ + /// Succinct description of the rule. + /// + /// Put context and details about the rule. + /// As a starting point, you can take the description of the corresponding _ESLint_ rule (if any). + /// + /// Try to stay consistent with the descriptions of implemented rules. + /// + /// Add a link to the corresponding stylelint rule (if any): + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```css,expect_diagnostic + /// p {{}} + /// ``` + /// + /// ### Valid + /// + /// ```css + /// p {{ + /// color: red; + /// }} + /// ``` + /// + pub {rule_name_upper_camel} {{ + version: "next", + name: "{rule_name_lower_camel}", + recommended: false, + }} +}} + +impl Rule for {rule_name_upper_camel} {{ + type Query = Ast; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Option {{ + let node = ctx.query(); + None + }} + + fn diagnostic(_: &RuleContext, node: &Self::State) -> Option {{ + // + // Read our guidelines to write great diagnostics: + // https://docs.rs/biome_analyze/latest/biome_analyze/#what-a-rule-should-say-to-the-user + // + let span = node.range(); + Some( + RuleDiagnostic::new( + rule_category!(), + span, + markup! {{ + "Unexpected empty block is not allowed" + }}, + ) + .note(markup! {{ + "This note will give you more information." + }}), + ) + }} +}} +"# + ) } } } -pub fn generate_new_lintrule(kind: RuleKind, rule_name: &str) { +pub fn generate_new_analyzer_rule(kind: RuleKind, category: Category, rule_name: &str) { let rule_kind = kind.as_str(); let crate_folder = project_root().join(format!("crates/biome_{rule_kind}_analyze")); - let rule_folder = crate_folder.join("src/lint/nursery"); + let rule_folder = match &category { + Category::Lint => crate_folder.join("src/lint/nursery"), + Category::Assist => crate_folder.join("src/assists/nursery"), + }; let test_folder = crate_folder.join("tests/specs/nursery"); let rule_name_upper_camel = rule_name.to_camel(); let rule_name_snake = rule_name.to_snake(); diff --git a/xtask/codegen/src/lib.rs b/xtask/codegen/src/lib.rs index 48eda1e4b21..4f9c66afc60 100644 --- a/xtask/codegen/src/lib.rs +++ b/xtask/codegen/src/lib.rs @@ -5,7 +5,7 @@ mod css_kinds_src; mod formatter; mod generate_analyzer; mod generate_macros; -pub mod generate_new_lintrule; +pub mod generate_new_analyzer_rule; mod generate_node_factory; mod generate_nodes; mod generate_nodes_mut; @@ -28,13 +28,14 @@ mod unicode; use bpaf::Bpaf; use std::path::Path; +use crate::generate_new_analyzer_rule::Category; use xtask::{glue::fs2, Mode, Result}; pub use self::ast::generate_ast; pub use self::formatter::generate_formatters; pub use self::generate_analyzer::generate_analyzer; pub use self::generate_crate::generate_crate; -pub use self::generate_new_lintrule::{generate_new_lintrule, RuleKind}; +pub use self::generate_new_analyzer_rule::{generate_new_analyzer_rule, RuleKind}; pub use self::parser_tests::generate_parser_tests; pub use self::unicode::generate_tables; @@ -104,14 +105,19 @@ pub enum TaskCommand { Unicode, /// Creates a new lint rule #[bpaf(command, long("new-lintrule"))] - NewLintRule( + NewRule { /// Path of the rule #[bpaf(long("kind"))] - RuleKind, + kind: RuleKind, + /// Name of the rule #[bpaf(long("name"))] - String, - ), + name: String, + + /// Name of the rule + #[bpaf(long("category"))] + category: Category, + }, /// Promotes a nursery rule #[bpaf(command, long("promote-rule"))] PromoteRule { diff --git a/xtask/codegen/src/main.rs b/xtask/codegen/src/main.rs index 672b11ba433..14a035ce1d1 100644 --- a/xtask/codegen/src/main.rs +++ b/xtask/codegen/src/main.rs @@ -29,8 +29,8 @@ use crate::promote_rule::promote_rule; use xtask::Mode::Overwrite; use xtask_codegen::{ - generate_analyzer, generate_ast, generate_crate, generate_formatters, generate_new_lintrule, - generate_parser_tests, generate_tables, task_command, TaskCommand, + generate_analyzer, generate_ast, generate_crate, generate_formatters, + generate_new_analyzer_rule, generate_parser_tests, generate_tables, task_command, TaskCommand, }; fn main() -> Result<()> { @@ -73,8 +73,12 @@ fn main() -> Result<()> { TaskCommand::Unicode => { generate_tables()?; } - TaskCommand::NewLintRule(new_rule_kind, rule_name) => { - generate_new_lintrule(new_rule_kind, &rule_name); + TaskCommand::NewRule { + category, + name, + kind, + } => { + generate_new_analyzer_rule(kind, category, &name); } TaskCommand::PromoteRule { name, group } => { promote_rule(&name, &group);