diff --git a/Cargo.lock b/Cargo.lock index ba51dba88123..4adcb27bca48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -745,12 +745,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 ec3a5e0a3df9..428f80f97434 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -165,6 +165,7 @@ ignore = "0.4.21" indexmap = { version = "2.2.6", features = ["serde"] } insta = "1.39.0" lazy_static = "1.4.0" +natord = "1.0.9" oxc_resolver = "1.8.1" proc-macro2 = "1.0.85" quickcheck = "1.0.3" @@ -187,7 +188,6 @@ tracing = { version = "0.1.40", default-features = false, features = tracing-subscriber = "0.3.18" unicode-bom = "2.0.3" unicode-width = "0.1.12" - [profile.dev.package.biome_wasm] debug = true opt-level = "s" diff --git a/crates/biome_analyze/src/categories.rs b/crates/biome_analyze/src/categories.rs index b8ea375d38bf..2045c8e48e7e 100644 --- a/crates/biome_analyze/src/categories.rs +++ b/crates/biome_analyze/src/categories.rs @@ -108,7 +108,9 @@ impl ActionCategory { } } -/// The sub-category of a refactor code action +/// The sub-category of a refactor code action. +/// +/// [Check the LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionKind) for more information: #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr( feature = "serde", diff --git a/crates/biome_analyze/src/rule.rs b/crates/biome_analyze/src/rule.rs index cfccb4a0d6d9..f7f1f4ea12fb 100644 --- a/crates/biome_analyze/src/rule.rs +++ b/crates/biome_analyze/src/rule.rs @@ -408,7 +408,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 {} @@ -442,6 +442,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 @@ -880,7 +917,7 @@ impl RuleAction { pub fn new( category: ActionCategory, applicability: impl Into, - message: impl biome_console::fmt::Display, + message: impl Display, mutation: BatchMutation, ) -> Self { Self { diff --git a/crates/biome_css_analyze/src/lint/nursery.rs b/crates/biome_css_analyze/src/lint/nursery.rs index b5b6861bf249..f48f45aee78b 100644 --- a/crates/biome_css_analyze/src/lint/nursery.rs +++ b/crates/biome_css_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_duplicate_at_import_rules; pub mod no_duplicate_font_names; @@ -19,7 +19,7 @@ pub mod no_unmatchable_anb_selector; pub mod use_consistent_grid_areas; pub mod use_generic_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 e8a774589b0d..94f6f2b77a94 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -269,7 +269,11 @@ 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", - ; + ; // end lint rules + + "assists/nursery/useSortedKeys", + // end assist rules + // General categories "files/missingHandler", "format", diff --git a/crates/biome_js_analyze/Cargo.toml b/crates/biome_js_analyze/Cargo.toml index 8e3a424f82aa..055b8f61cc36 100644 --- a/crates/biome_js_analyze/Cargo.toml +++ b/crates/biome_js_analyze/Cargo.toml @@ -29,7 +29,7 @@ biome_unicode_table = { workspace = true } bitflags = { workspace = true } enumflags2 = { workspace = true } lazy_static = { workspace = true } -natord = "1.0.9" +natord = { workspace = true } regex = { workspace = true } roaring = "0.10.5" rustc-hash = { workspace = true } diff --git a/crates/biome_js_analyze/src/assists/correctness.rs b/crates/biome_js_analyze/src/assists/correctness.rs index 4fe923edaceb..4929a52b156e 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 57cfc1baf88e..83fc66360d56 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 82cfdba5edc5..450b28e23e9c 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; @@ -32,7 +32,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 ae2c899036c2..d6a5d31a826b 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; @@ -44,7 +44,7 @@ pub mod use_jsx_key_in_iterable; 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 4dc25dd91679..6194638ba2ef 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_console; pub mod no_done_callback; @@ -32,7 +32,7 @@ pub mod use_throw_new_error; pub mod use_throw_only_error; pub mod use_top_level_regex; -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 852adc0408ba..c3b28a5db455 100644 --- a/crates/biome_js_analyze/src/lint/performance.rs +++ b/crates/biome_js_analyze/src/lint/performance.rs @@ -1,13 +1,13 @@ //! 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_barrel_file; pub mod no_delete; pub mod no_re_export_all; -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 75abfba14595..309d737734ac 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 56534d245418..d8fb0e8152dd 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; @@ -46,7 +46,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 a65e09b737bf..b193f9ef2bd8 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; @@ -58,7 +58,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 18f9631b4d66..641fc626bd43 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 00faddee221f..fa59ba13e3fd 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 a645de57ec1f..7e818dbb375d 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 000000000000..120fa61115f5 --- /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 000000000000..993ff89ebc6e --- /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 000000000000..f036fbc5c73b --- /dev/null +++ b/crates/biome_json_analyze/src/assists/nursery/use_sorted_keys.rs @@ -0,0 +1,136 @@ +use crate::JsonRuleAction; +use biome_analyze::{ + context::RuleContext, declare_rule, ActionCategory, Ast, FixKind, RefactorKind, Rule, + RuleAction, +}; +use biome_console::markup; +use biome_json_factory::make::{json_member_list, token}; +use biome_json_syntax::{JsonMember, JsonMemberList, T}; +use biome_rowan::{AstNode, AstNodeExt, AstSeparatedList, BatchMutationExt}; +use std::borrow::Cow; +use std::cmp::Ordering; +use std::collections::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): + /// + pub UseSortedKeys { + version: "next", + name: "useSortedKeys", + language: "json", + recommended: false, + fix_kind: FixKind::Safe, + } +} + +#[derive(Eq, PartialEq)] +pub struct MemberKey { + node: JsonMember, +} + +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 { + separators.push(token(T![,])) + } + } + + json_member_list(items, separators) + } +} + +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 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::new( + ActionCategory::Refactor(RefactorKind::Other(Cow::Borrowed("useSortedKeys"))), + ctx.metadata().applicability(), + markup! { + "They keys of the current object can be sorted." + }, + mutation, + )) + } +} diff --git a/crates/biome_json_analyze/src/lib.rs b/crates/biome_json_analyze/src/lib.rs index 216f9cb27d6c..fb2340e49e4c 100644 --- a/crates/biome_json_analyze/src/lib.rs +++ b/crates/biome_json_analyze/src/lib.rs @@ -1,4 +1,6 @@ +mod assists; mod lint; + pub mod options; mod registry; mod suppression_action; @@ -8,11 +10,13 @@ pub use crate::registry::visit_registry; use crate::suppression_action::JsonSuppressionAction; 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 1de83c6e36d6..d64e3a786161 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 5df3b0cfc100..a5f182e1e875 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 000000000000..202d76e1ddfc --- /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 000000000000..06fff63065eb --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/nursery/useSortedKeys/invalid.json.snap @@ -0,0 +1,24 @@ +--- +source: crates/biome_json_analyze/tests/spec_tests.rs +expression: invalid.json +--- +# Input +```json +{ + "zed": "", + "alpha": "fff" +} + +``` + +# Actions +```diff +@@ -1,4 +1,4 @@ + { +- "zed": "", +- "alpha": "fff" ++ "alpha": "fff", ++ "zed": "" + } + +``` 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 000000000000..0967ef424bce --- /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 000000000000..e6306f075cec --- /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_service/src/file_handlers/json.rs b/crates/biome_service/src/file_handlers/json.rs index 754c141e578d..0e8d5e3bc034 100644 --- a/crates/biome_service/src/file_handlers/json.rs +++ b/crates/biome_service/src/file_handlers/json.rs @@ -32,6 +32,7 @@ use biome_json_syntax::{JsonLanguage, JsonRoot, JsonSyntaxNode}; use biome_parser::AnyParse; use biome_rowan::{AstNode, NodeCache}; use biome_rowan::{TextRange, TextSize, TokenAtOffset}; +use tracing::{debug_span, trace_span}; #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -428,10 +429,23 @@ fn lint(params: LintParams) -> LintResults { }) } -fn code_actions(_: CodeActionsParams) -> PullActionsResult { - PullActionsResult { - actions: Vec::new(), - } +fn code_actions(params: CodeActionsParams) -> PullActionsResult { + let CodeActionsParams { + parse, + range, + workspace: _, + path, + manifest: _, + language: _, + settings: _, + } = params; + + debug_span!("Code actions JSON", range =? range, path =? path).in_scope(move || { + let tree: JsonRoot = parse.tree(); + trace_span!("Parsed file", tree =? tree).in_scope(move || PullActionsResult { + actions: Vec::new(), + }) + }) } fn fix_all(params: FixAllParams) -> Result { diff --git a/justfile b/justfile index 0f7f92d894cf..bd8b899e334d 100644 --- a/justfile +++ b/justfile @@ -58,13 +58,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 6f1a4a2c75c1..ed840f2bce2d 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -2494,6 +2494,7 @@ export type Category = | "lint/suspicious/useIsArray" | "lint/suspicious/useNamespaceKeyword" | "lint/suspicious/useValidTypeof" + | "assists/nursery/useSortedKeys" | "files/missingHandler" | "format" | "check" @@ -2651,8 +2652,10 @@ export interface CodeSuggestion { suggestion: TextEdit; } /** - * The sub-category of a refactor code action - */ + * The sub-category of a refactor code action. + +[Check the LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionKind) for more information: + */ export type RefactorKind = | "None" | "Extract" diff --git a/xtask/codegen/src/generate_analyzer.rs b/xtask/codegen/src/generate_analyzer.rs index 3f938a305e51..7a3458511750 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,28 @@ 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 }; + 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 +255,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 69% rename from xtask/codegen/src/generate_new_lintrule.rs rename to xtask/codegen/src/generate_new_analyzer_rule.rs index c5ae8dfc9e66..f37b8eb015df 100644 --- a/xtask/codegen/src/generate_new_lintrule.rs +++ b/xtask/codegen/src/generate_new_analyzer_rule.rs @@ -32,6 +32,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, @@ -193,18 +213,91 @@ 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::JsonMember; +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(ctx: &RuleContext, _state: &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 = ctx.query().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_name_camel = Case::Camel.convert(rule_name); 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 test_folder = crate_folder.join("tests/specs/nursery"); - + let rule_folder = match &category { + Category::Lint => crate_folder.join("src/lint/nursery"), + Category::Assist => crate_folder.join("src/assists/nursery"), + }; // Generate rule code let code = generate_rule_template( &kind, @@ -224,11 +317,20 @@ pub fn generate_new_lintrule(kind: RuleKind, rule_name: &str) { if !categories.contains(&rule_name_camel) { let kebab_case_rule = Case::Kebab.convert(&rule_name_camel); // We sort rules to reduce conflicts between contributions made in parallel. - let rule_line = format!( - r#" "lint/nursery/{rule_name_camel}": "https://biomejs.dev/linter/rules/{kebab_case_rule}","# - ); - let lint_start = "define_categories! {\n"; - let lint_end = "\n ;\n"; + let rule_line = match category { + Category::Lint => format!( + r#" "lint/nursery/{rule_name_camel}": "https://biomejs.dev/linter/rules/{kebab_case_rule}","# + ), + Category::Assist => format!(r#" "assists/nursery/{rule_name_camel}","#), + }; + let lint_start = match category { + Category::Lint => "define_categories! {\n", + Category::Assist => " ; // end lint rules\n\n", + }; + let lint_end = match category { + Category::Lint => "\n ; // end lint rules\n", + Category::Assist => "\n // end assist rules\n", + }; debug_assert!(categories.contains(lint_start)); debug_assert!(categories.contains(lint_end)); let lint_start_index = categories.find(lint_start).unwrap() + lint_start.len(); diff --git a/xtask/codegen/src/lib.rs b/xtask/codegen/src/lib.rs index 3dca82481d75..40b91a9a0f03 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; @@ -29,13 +29,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; @@ -105,14 +106,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 904f8b26dea4..a8be913bb818 100644 --- a/xtask/codegen/src/main.rs +++ b/xtask/codegen/src/main.rs @@ -25,8 +25,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<()> { @@ -69,8 +69,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);