From 959db97aad3beeacd995168389c2e4421f777031 Mon Sep 17 00:00:00 2001 From: Victorien Elvinger Date: Sun, 5 Nov 2023 13:53:35 +0100 Subject: [PATCH] refactor(deserialize): new abstractions for generic deserialization --- Cargo.lock | 3 + crates/biome_analyze/CONTRIBUTING.md | 170 +- crates/biome_cli/src/commands/rage.rs | 1 + crates/biome_cli/src/configuration.rs | 9 +- .../line_width_parse_errors_overflow.snap | 2 +- .../main_configuration/line_width_error.snap | 5 +- crates/biome_deserialize/Cargo.toml | 1 + crates/biome_deserialize/src/diagnostics.rs | 60 +- crates/biome_deserialize/src/impls.rs | 571 +++ crates/biome_deserialize/src/json.rs | 685 +--- crates/biome_deserialize/src/lib.rs | 349 +- crates/biome_deserialize/src/string_set.rs | 119 +- crates/biome_deserialize/src/visitor.rs | 39 - crates/biome_formatter/Cargo.toml | 1 + .../src/format_element/document.rs | 2 +- crates/biome_formatter/src/lib.rs | 32 +- crates/biome_formatter_test/src/spec.rs | 4 +- .../src/test_prettier_snapshot.rs | 2 +- .../no_excessive_cognitive_complexity.rs | 81 +- crates/biome_js_analyze/src/options.rs | 42 +- .../use_exhaustive_dependencies.rs | 184 +- .../style/no_restricted_globals.rs | 64 +- .../style/use_naming_convention.rs | 102 +- .../invalidConfig.js.snap | 2 +- crates/biome_js_formatter/Cargo.toml | 1 + crates/biome_js_formatter/src/context.rs | 140 +- .../src/context/trailing_comma.rs | 41 +- .../src/utils/assignment_like.rs | 2 +- crates/biome_json_formatter/src/context.rs | 2 +- crates/biome_lsp/src/session.rs | 1 + .../biome_project/src/node_js_project/mod.rs | 2 +- .../src/node_js_project/package_json.rs | 224 +- .../invalid/lisence_not_string.json.snap | 2 +- .../tests/invalid/name.json.snap | 2 +- .../src/configuration/diagnostics.rs | 3 +- .../src/configuration/formatter.rs | 2 +- .../configuration/parse/json/configuration.rs | 164 +- .../src/configuration/parse/json/files.rs | 86 +- .../src/configuration/parse/json/formatter.rs | 180 +- .../parse/json/javascript/formatter.rs | 192 +- .../parse/json/javascript/mod.rs | 171 +- .../configuration/parse/json/json_impl/mod.rs | 235 +- .../src/configuration/parse/json/linter.rs | 286 +- .../src/configuration/parse/json/mod.rs | 34 - .../parse/json/organize_imports.rs | 77 +- .../src/configuration/parse/json/overrides.rs | 383 +- .../src/configuration/parse/json/rules.rs | 3512 +++++++++-------- .../src/configuration/parse/json/vcs.rs | 139 +- .../files_ignore_incorrect_type.json.snap | 2 +- .../files_include_incorrect_type.json.snap | 2 +- .../invalid/files_incorrect_type.json.snap | 2 +- .../files_incorrect_type_for_value.json.snap | 2 +- .../invalid/files_negative_max_size.json.snap | 4 +- ...ormat_with_errors_incorrect_type.json.snap | 2 +- .../formatter_incorrect_type.json.snap | 2 +- .../formatter_line_width_too_high.json.snap | 4 +- ...ne_width_too_higher_than_allowed.json.snap | 5 +- .../tests/invalid/organize_imports.json.snap | 2 +- .../overrides/incorrect_type.json.snap | 2 +- .../incorrect_value_javascript.json.snap | 2 +- .../tests/invalid/schema.json.snap | 2 +- .../invalid/vcs_incorrect_type.json.snap | 2 +- .../invalid/vcs_missing_client.json.snap | 2 +- .../invalid/wrong_extends_type.json.snap | 2 +- crates/biome_test_utils/src/lib.rs | 2 +- editors/vscode/configuration_schema.json | 2 +- .../@biomejs/biome/configuration_schema.json | 2 +- xtask/codegen/src/generate_configuration.rs | 172 +- 68 files changed, 4888 insertions(+), 3740 deletions(-) create mode 100644 crates/biome_deserialize/src/impls.rs delete mode 100644 crates/biome_deserialize/src/visitor.rs diff --git a/Cargo.lock b/Cargo.lock index d87de6dd98a5..2969a11fb8d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,7 @@ dependencies = [ "biome_json_parser", "biome_json_syntax", "biome_rowan", + "bitflags 2.3.1", "indexmap", "schemars", "serde", @@ -322,6 +323,7 @@ name = "biome_formatter" version = "0.2.0" dependencies = [ "biome_console", + "biome_deserialize", "biome_diagnostics", "biome_js_parser", "biome_js_syntax", @@ -418,6 +420,7 @@ dependencies = [ name = "biome_js_formatter" version = "0.2.0" dependencies = [ + "biome_console", "biome_deserialize", "biome_diagnostics", "biome_diagnostics_categories", diff --git a/crates/biome_analyze/CONTRIBUTING.md b/crates/biome_analyze/CONTRIBUTING.md index 30a15566d2ef..093c4df19a75 100644 --- a/crates/biome_analyze/CONTRIBUTING.md +++ b/crates/biome_analyze/CONTRIBUTING.md @@ -300,20 +300,144 @@ just ready ### Rule configuration Some rules may allow customization using configuration. -The first step is to setup a struct to represent the rule configuration. +Biome tries to introduce a minimum of the rule configuration. +Before adding an option discuss that. + +The first step is to create the data representation of the rule's configuration. + +```rust,ignore +#[derive(Debug, Default, Clone)] +pub struct GreatRuleOptions { + main_behavior: Behavior, + extra_behaviors: Vec, +} + +#[derive(Debug, Default, Clone)] +pub enum Behavior { + #[default] + A, + B, + C, +} +``` + +You also need to picture the equivalent data representation in _JSON_: + +```json +{ + "mainBehavior": "A", + "extraBehaviors": ["C"] +} +``` + +So, you have to implement the `Deserializable` trait for these two types. +An implementation can reuse an existing type that implements `Deserializable`. +For example, we could deserialize `Behavior` by first deserializing a string, +and then checking that the string is either `A`, `B`, or `C`. +This is what we do in the following code snippet. +Note that, instead of using `String`, we use `TokenText`. +This avoids a string allocation. ```rust,ignore -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReactExtensiveDependenciesOptions { - hooks_config: FxHashMap, - stable_config: FxHashSet, +impl Deserializable for Behavior { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + const ALLOWED_VARIANTS: &[&str] = &["A", "B", "C"]; + let range = value.range(); + let value = TokenText::deserialize(value, diagnostics)?; + match value.text() { + "A" => Some(Behavior.A), + "B" => Some(Behavior.B), + "C" => Some(Behavior.C), + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_value( + value.text(), + range, + ALLOWED_VARIANTS, + )); + None + } + } + } } +``` + +Implementing `Deserializable` for `GreatRuleOptions` requires more work, +because we cannot rely on an existing deserializable type. +We have to use a _deserialization visitor_. +We create a visitor by creating a zero-sized `struct` that implements `DeserializationVisitor`. +A visitor must specify the type that it produces in its associated type `Output`. +Here the visitor produces a `GreatRuleOptions`. +It must also specify which type is expected with the associated constant `EXPECTED_TYPE`. +Here we deserialize an object (a _map_ of string-value pairs). +Thus, it expects a `ExpectedType::MAP`. +So we implement `visit_map` that traverses the key-value pairs, +deserializes every key as a string (a token text to avoid allocating a string), +and deserializes the value based on the key. -impl Rule for UseExhaustiveDependencies { +```rust,ignore +impl Deserializable for GreatRuleOptions { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(GreatRuleOptionsVisitor, diagnostics) + } +} + +struct GreatRuleOptionsVisitor; +impl DeserializationVisitor for GreatRuleOptionsVisitor { + type Output = GreatRuleOptions; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + + fn visit_map( + self, + members: impl Iterator, + _range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["mainBehavior", "extraBehavior"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "mainBehavior" => { + if let Some(strict_case) = Deserialize::deserialize(value, diagnostics) { + result.main_behavior = value; + } + } + "extraBehavior" => { + if let Some(enum_member_case) = Deserialize::deserialize(value, diagnostics) { + result.extra_behavior = enum_member_case; + } + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), + } + } + Some(result) + } +} +``` + +Once done, you can set the associated type `Options` of the rule: + + +```rust,ignore +impl Rule for GreatRule { type Query = Semantic; type State = Fix; type Signals = Vec; - type Options = ReactExtensiveDependenciesOptions; + type Options = GreatRuleOptions; ... } @@ -327,10 +451,10 @@ This allows the rule to be configured inside `biome.json` file like: "rules": { "recommended": true, "nursery": { - "useExhaustiveDependencies": { + "greatRule": { "level": "error", "options": { - "hooks": [["useMyEffect", 0, 1]] + "mainBehavior": "A" } } } @@ -345,6 +469,34 @@ A rule can retrieve its option with: let options = ctx.options(); ``` +The compiler should warn you that `GreatRuleOptions` does not implement some required types. +We currently require implementing _serde_'s traits `Deserialize`/`Serialize` and _Bpaf_'s parser trait. +You can simply use a derive macros: + +```rust,ignore +#[derive(Debug, Default, Clone, Serialize, Deserialize, Bpaf)] +#[cfg_attr(feature = "schemars", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct GreatRuleOptions { + #[bpaf(hide)] + #[serde(default, skip_serializing_if = "is_default")] + main_behavior: Behavior, + + #[bpaf(hide)] + #[serde(default, skip_serializing_if = "is_default")] + extra_behaviors: Vec, +} + +#[derive(Debug, Default, Clone)] +#[cfg_attr(feature = "schemars", derive(JsonSchema))] +pub enum Behavior { + #[default] + A, + B, + C, +} +``` + ### Deprecate a rule There are occasions when a rule must be deprecated, to avoid breaking changes. The reason diff --git a/crates/biome_cli/src/commands/rage.rs b/crates/biome_cli/src/commands/rage.rs index 69c17ca967f3..a2891f1c93a2 100644 --- a/crates/biome_cli/src/commands/rage.rs +++ b/crates/biome_cli/src/commands/rage.rs @@ -174,6 +174,7 @@ impl Display for RageConfiguration<'_, '_> { Ok(None) => KeyValuePair("Status", markup!("unset")).fmt(fmt)?, Ok(Some(result)) => { let (configuration, diagnostics) = result.deserialized.consume(); + let configuration = configuration.unwrap_or_default(); let status = if !diagnostics.is_empty() { for diagnostic in diagnostics { (markup! { diff --git a/crates/biome_cli/src/configuration.rs b/crates/biome_cli/src/configuration.rs index d53f2771b3fa..3a5d3674def4 100644 --- a/crates/biome_cli/src/configuration.rs +++ b/crates/biome_cli/src/configuration.rs @@ -31,8 +31,11 @@ impl LoadedConfiguration { /// If a configuration can't be resolved from the file system, the operation will fail. pub fn apply_extends(mut self, fs: &DynRef) -> Result { let deserialized = self.deserialize_extends(fs)?; - let (configurations, errors): (Vec<_>, Vec<_>) = - deserialized.into_iter().map(|d| d.consume()).unzip(); + let (configurations, errors): (Vec<_>, Vec<_>) = deserialized + .into_iter() + .map(|d| d.consume()) + .map(|(config, diagnostics)| (config.unwrap_or_default(), diagnostics)) + .unzip(); let extended_configuration = configurations.into_iter().reduce( |mut previous_configuration, current_configuration| { @@ -172,7 +175,7 @@ impl From> for LoadedConfiguration { } = value; let (configuration, diagnostics) = deserialized.consume(); LoadedConfiguration { - configuration, + configuration: configuration.unwrap_or_default(), diagnostics, directory_path: Some(configuration_directory_path), file_path: Some(configuration_file_path), diff --git a/crates/biome_cli/tests/snapshots/main_commands_format/line_width_parse_errors_overflow.snap b/crates/biome_cli/tests/snapshots/main_commands_format/line_width_parse_errors_overflow.snap index 838132f93a27..96753a92f715 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_format/line_width_parse_errors_overflow.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_format/line_width_parse_errors_overflow.snap @@ -10,7 +10,7 @@ flags/invalid ━━━━━━━━━━━━━━━━━━━━━━ × Failed to parse CLI arguments. Caused by: - couldn't parse `321`: The line width exceeds the maximum value (320) + couldn't parse `321`: The line width should be between 1 and 320 diff --git a/crates/biome_cli/tests/snapshots/main_configuration/line_width_error.snap b/crates/biome_cli/tests/snapshots/main_configuration/line_width_error.snap index daae16f8abd7..e76fa48a77b5 100644 --- a/crates/biome_cli/tests/snapshots/main_configuration/line_width_error.snap +++ b/crates/biome_cli/tests/snapshots/main_configuration/line_width_error.snap @@ -28,8 +28,7 @@ configuration ━━━━━━━━━━━━━━━━━━━━━━ ```block biome.json:3:18 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The line width exceeds the maximum value (320) - + × The number should be an integer between 1 and 320. 1 │ { 2 │ "formatter": { @@ -38,8 +37,6 @@ biome.json:3:18 deserialize ━━━━━━━━━━━━━━━━━ 4 │ } 5 │ } - i Maximum value accepted is 320 - ``` diff --git a/crates/biome_deserialize/Cargo.toml b/crates/biome_deserialize/Cargo.toml index b7f08babfb67..05ae68264659 100644 --- a/crates/biome_deserialize/Cargo.toml +++ b/crates/biome_deserialize/Cargo.toml @@ -18,6 +18,7 @@ biome_diagnostics = { workspace = true } biome_json_parser = { workspace = true } biome_json_syntax = { workspace = true } biome_rowan = { workspace = true } +bitflags = { workspace = true } indexmap = { workspace = true, features = ["serde"] } schemars = { workspace = true, optional = true } serde = { workspace = true } diff --git a/crates/biome_deserialize/src/diagnostics.rs b/crates/biome_deserialize/src/diagnostics.rs index ca4693ee06e0..c62680cf037e 100644 --- a/crates/biome_deserialize/src/diagnostics.rs +++ b/crates/biome_deserialize/src/diagnostics.rs @@ -5,8 +5,47 @@ use biome_diagnostics::{ Advices, Diagnostic, DiagnosticTags, LogCategory, MessageAndDescription, Severity, Visit, }; use biome_rowan::{SyntaxError, TextRange}; +use bitflags::bitflags; use serde::{Deserialize, Serialize}; +bitflags! { + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + pub struct ExpectedType: u8 { + const NULL = 1 << 0; + const BOOL = 1 << 1; + const NUMBER = 1 << 2; + const STR = 1 << 3; + const ARRAY = 1 << 4; + const MAP = 1 << 5; + } +} + +impl Display for ExpectedType { + fn fmt(&self, fmt: &mut biome_console::fmt::Formatter) -> std::io::Result<()> { + if self.is_empty() { + return write!(fmt, "no value"); + } + let mut is_not_first = false; + for expected_type in self.iter() { + if is_not_first { + write!(fmt, " or ")?; + } + let expected_type = match expected_type { + ExpectedType::NULL => "null", + ExpectedType::BOOL => "a boolean", + ExpectedType::NUMBER => "a number", + ExpectedType::STR => "a string", + ExpectedType::ARRAY => "an array", + ExpectedType::MAP => "an object", + _ => unreachable!("Unhandled deserialization type."), + }; + write!(fmt, "{}", expected_type)?; + is_not_first = true; + } + Ok(()) + } +} + /// Diagnostic emitted during the deserialization #[derive(Debug, Serialize, Clone, Deserialize, Diagnostic)] #[diagnostic(category = "deserialize")] @@ -35,21 +74,22 @@ impl DeserializationDiagnostic { } } - /// Emitted when the type of a value is incorrect. - pub fn new_incorrect_type_for_value( - key_name: impl Display, - expected_type: impl Display, - range: impl AsSpan, - ) -> Self { + /// Emitted when a generic node has an incorrect type + pub fn new_incorrect_type(expected_type: ExpectedType, range: impl AsSpan) -> Self { Self::new(markup! { - "The value of key "{{key_name}}" is incorrect. Expected a "{{expected_type}}"." - }).with_range(range) + "Incorrect type, expected "{expected_type}"." + }) + .with_range(range) } /// Emitted when a generic node has an incorrect type - pub fn new_incorrect_type(expected_type: impl Display, range: impl AsSpan) -> Self { + pub fn new_out_of_bound_integer( + min: impl Display, + max: impl Display, + range: impl AsSpan, + ) -> Self { Self::new(markup! { - "Incorrect type, expected a "{{expected_type}}"." + "The number should be an integer between "{min}" and "{max}"." }) .with_range(range) } diff --git a/crates/biome_deserialize/src/impls.rs b/crates/biome_deserialize/src/impls.rs new file mode 100644 index 000000000000..87cc2c455aaf --- /dev/null +++ b/crates/biome_deserialize/src/impls.rs @@ -0,0 +1,571 @@ +use crate::{ + diagnostics::ExpectedType, Deserializable, DeserializableValue, DeserializationDiagnostic, + DeserializationVisitor, +}; +use biome_rowan::{TextRange, TokenText}; +use indexmap::IndexSet; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + hash::{BuildHasher, Hash}, + marker::PhantomData, + num::{NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize}, + path::PathBuf, +}; + +/// Implementation of [Deserializable] for common data structures. + +/// A String representation of a number (integer, float). +/// The format should be parsable by Rust numeric types. +#[derive(Eq, PartialEq, Clone)] +pub struct TokenNumber(pub(crate) TokenText); +impl TokenNumber { + pub fn text(&self) -> &str { + self.0.text() + } +} +impl Deserializable for TokenNumber { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = TokenNumber; + const EXPECTED_TYPE: ExpectedType = ExpectedType::NUMBER; + fn visit_number( + self, + value: TokenNumber, + _range: TextRange, + _diagnostics: &mut Vec, + ) -> Option { + Some(value) + } + } + value.deserialize(Visitor, diagnostics) + } +} + +impl Deserializable for () { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + ExpectedType::empty(), + value.range(), + )); + None + } +} + +impl Deserializable for bool { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = bool; + const EXPECTED_TYPE: ExpectedType = ExpectedType::BOOL; + fn visit_bool( + self, + value: bool, + _range: TextRange, + _diagnostics: &mut Vec, + ) -> Option { + Some(value) + } + } + value.deserialize(Visitor, diagnostics) + } +} + +impl Deserializable for f32 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new("The number should be a float representable on 32 bits") + .with_range(range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for f64 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new("The number should be a float representable on 64 bits") + .with_range(range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for i8 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new_out_of_bound_integer(Self::MIN, Self::MAX, range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for i16 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new_out_of_bound_integer(Self::MIN, Self::MAX, range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for i32 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new_out_of_bound_integer(Self::MIN, Self::MAX, range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for isize { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new_out_of_bound_integer(Self::MIN, Self::MAX, range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for i64 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new_out_of_bound_integer(Self::MIN, Self::MAX, range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for u8 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new_out_of_bound_integer(Self::MIN, Self::MAX, range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for u16 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new_out_of_bound_integer(Self::MIN, Self::MAX, range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for u32 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new_out_of_bound_integer(Self::MIN, Self::MAX, range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for usize { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new_out_of_bound_integer(Self::MIN, Self::MAX, range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for u64 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new_out_of_bound_integer(Self::MIN, Self::MAX, range); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for NonZeroU8 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = DeserializationDiagnostic::new_out_of_bound_integer( + Self::MIN.get(), + Self::MAX.get(), + range, + ); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for NonZeroU16 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = DeserializationDiagnostic::new_out_of_bound_integer( + Self::MIN.get(), + Self::MAX.get(), + range, + ); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for NonZeroU32 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = DeserializationDiagnostic::new_out_of_bound_integer( + Self::MIN.get(), + Self::MAX.get(), + range, + ); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for NonZeroUsize { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = DeserializationDiagnostic::new_out_of_bound_integer( + Self::MIN.get(), + Self::MAX.get(), + range, + ); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for NonZeroU64 { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = DeserializationDiagnostic::new_out_of_bound_integer( + Self::MIN.get(), + Self::MAX.get(), + range, + ); + diagnostics.push(diagnostic); + None + } +} + +impl Deserializable for TokenText { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = TokenText; + const EXPECTED_TYPE: ExpectedType = ExpectedType::STR; + fn visit_str( + self, + value: TokenText, + _range: TextRange, + _diagnostics: &mut Vec, + ) -> Option { + Some(value) + } + } + value.deserialize(Visitor, diagnostics) + } +} + +impl Deserializable for String { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + TokenText::deserialize(value, diagnostics).map(|value| value.to_string()) + } +} + +impl Deserializable for PathBuf { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + String::deserialize(value, diagnostics).map(PathBuf::from) + } +} + +impl Deserializable for Vec { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + struct Visitor(PhantomData); + impl DeserializationVisitor for Visitor { + type Output = Vec; + const EXPECTED_TYPE: ExpectedType = ExpectedType::ARRAY; + fn visit_array( + self, + values: impl Iterator, + _range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + Some( + values + .filter_map(|value| Deserializable::deserialize(value, diagnostics)) + .collect(), + ) + } + } + value.deserialize(Visitor(PhantomData), diagnostics) + } +} + +impl Deserializable for HashSet { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + struct Visitor(PhantomData<(T, S)>); + impl DeserializationVisitor + for Visitor + { + type Output = HashSet; + const EXPECTED_TYPE: ExpectedType = ExpectedType::ARRAY; + fn visit_array( + self, + values: impl Iterator, + _range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + Some( + values + .filter_map(|value| Deserializable::deserialize(value, diagnostics)) + .collect(), + ) + } + } + value.deserialize(Visitor(PhantomData), diagnostics) + } +} + +impl Deserializable for IndexSet { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + struct Visitor(PhantomData); + impl DeserializationVisitor for Visitor { + type Output = IndexSet; + const EXPECTED_TYPE: ExpectedType = ExpectedType::ARRAY; + fn visit_array( + self, + values: impl Iterator, + _range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + Some( + values + .filter_map(|value| Deserializable::deserialize(value, diagnostics)) + .collect(), + ) + } + } + value.deserialize(Visitor(PhantomData), diagnostics) + } +} + +impl Deserializable + for HashMap +{ + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + struct Visitor(PhantomData<(K, V, S)>); + impl + DeserializationVisitor for Visitor + { + type Output = HashMap; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + _range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut result = Self::Output::default(); + for (key, value) in members { + let key = Deserializable::deserialize(key, diagnostics); + let value = Deserializable::deserialize(value, diagnostics); + if let (Some(key), Some(value)) = (key, value) { + result.insert(key, value); + } + } + Some(result) + } + } + value.deserialize(Visitor(PhantomData), diagnostics) + } +} + +impl Deserializable for BTreeMap { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + struct Visitor(PhantomData<(K, V)>); + impl DeserializationVisitor for Visitor { + type Output = BTreeMap; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + _range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut result = Self::Output::default(); + for (key, value) in members { + let key = Deserializable::deserialize(key, diagnostics); + let value = Deserializable::deserialize(value, diagnostics); + if let (Some(key), Some(value)) = (key, value) { + result.insert(key, value); + } + } + Some(result) + } + } + value.deserialize(Visitor(PhantomData), diagnostics) + } +} diff --git a/crates/biome_deserialize/src/json.rs b/crates/biome_deserialize/src/json.rs index 3c4d381869a2..fcbfe8af1d6c 100644 --- a/crates/biome_deserialize/src/json.rs +++ b/crates/biome_deserialize/src/json.rs @@ -1,574 +1,91 @@ -use crate::{DeserializationDiagnostic, Deserialized, VisitNode}; -use biome_console::markup; +use crate::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + Deserialized, TokenNumber, +}; use biome_diagnostics::{DiagnosticExt, Error}; use biome_json_parser::{parse_json, JsonParserOptions}; -use biome_json_syntax::{ - AnyJsonValue, JsonArrayValue, JsonBooleanValue, JsonLanguage, JsonMemberName, JsonNumberValue, - JsonObjectValue, JsonRoot, JsonStringValue, JsonSyntaxNode, -}; -use biome_rowan::{AstNode, AstSeparatedList, TextRange}; -use indexmap::IndexSet; -use std::num::ParseIntError; - -/// Main trait to -/// -pub trait JsonDeserialize: Sized { - /// It accepts a JSON AST and a visitor. The visitor is the [default](Default) implementation of the data - /// type that implements this trait. - fn deserialize_from_ast( - root: &JsonRoot, - visitor: &mut impl VisitJsonNode, - deserialize_diagnostics: &mut Vec, - ) -> Option<()>; -} - -impl JsonDeserialize for () { - fn deserialize_from_ast( - _root: &JsonRoot, - _visitor: &mut impl VisitJsonNode, - _deserialize_diagnostics: &mut Vec, - ) -> Option<()> { - Some(()) - } -} - -/// Convenient trait that contains utility functions to work with [JsonLanguage] -pub trait VisitJsonNode: VisitNode { - /// Convenient function to use inside [visit_map]. - /// - /// It casts `key` to [JsonMemberName] and the `value` to [AnyJsonValue]. - fn get_key_and_value( - &mut self, - key: &JsonSyntaxNode, - value: &JsonSyntaxNode, - ) -> Option<(JsonMemberName, AnyJsonValue)> { - Some(( - JsonMemberName::cast_ref(key)?, - AnyJsonValue::cast_ref(value)?, - )) - } - - /// It attempts to map a [AnyJsonValue] to a string. - /// - /// Use this function when you want to map a string to an enum type. - /// - /// ## Errors - /// - /// The function will emit a generic diagnostic if the `visitor` doesn't implement [visit_value] - fn map_to_known_string( - &mut self, - value: &AnyJsonValue, - name: &str, - diagnostics: &mut Vec, - ) -> Option<()> { - if JsonStringValue::can_cast(value.syntax().kind()) { - self.visit_value(value.syntax(), diagnostics)?; - return Some(()); - } - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "string", - value.range(), - )); - None - } - - /// It attempts to map a [AnyJsonValue] to a [String]. - /// - /// ## Errors - /// - /// It emits an error if `value` can't be cast to a [JsonStringValue] - fn map_to_string( - &self, - value: &AnyJsonValue, - name: &str, - diagnostics: &mut Vec, - ) -> Option { - JsonStringValue::cast_ref(value.syntax()) - .and_then(|node| Some(node.inner_string_text().ok()?.to_string())) - .or_else(|| { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "string", - value.range(), - )); - None - }) - } - - /// It attempts to map a [AnyJsonValue] to a [u8]. - /// - /// ## Errors - /// - /// It will fail if: - /// - `value` can't be cast to [JsonNumberValue] - /// - the value of the node can't be parsed to [u8] - fn map_to_u8( - &self, - value: &AnyJsonValue, - name: &str, - maximum: u8, - diagnostics: &mut Vec, - ) -> Option { - let value = JsonNumberValue::cast_ref(value.syntax()).or_else(|| { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "number", - value.range(), - )); - None - })?; - let value = value.value_token().ok()?; - let result = value.text_trimmed().parse::().map_err(|err| { - emit_diagnostic_form_number( - err, - value.text_trimmed(), - value.text_trimmed_range(), - maximum, - ) - }); - match result { - Ok(number) => Some(number), - Err(err) => { - diagnostics.push(err); - None - } - } - } - - /// It attempts to map a [AnyJsonValue] to a [usize]. - /// - /// ## Errors - /// - /// It will fail if: - /// - `value` can't be cast to [JsonNumberValue] - /// - the value of the node can't be parsed to [usize] - fn map_to_usize( - &self, - value: &AnyJsonValue, - name: &str, - maximum: usize, - diagnostics: &mut Vec, - ) -> Option { - let value = JsonNumberValue::cast_ref(value.syntax()).or_else(|| { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "number", - value.range(), - )); - None - })?; - let value = value.value_token().ok()?; - let result = value.text_trimmed().parse::().map_err(|err| { - emit_diagnostic_form_number( - err, - value.text_trimmed(), - value.text_trimmed_range(), - maximum, - ) - }); - match result { - Ok(number) => Some(number), - Err(err) => { - diagnostics.push(err); - None - } - } - } - - /// It attempts to map a [AnyJsonValue] to a [u16]. - /// - /// ## Errors - /// - /// It will fail if: - /// - `value` can't be cast to [JsonNumberValue] - /// - the value of the node can't be parsed to [u16] - fn map_to_u16( - &self, - value: &AnyJsonValue, - name: &str, - maximum: u16, - diagnostics: &mut Vec, - ) -> Option { - let value = JsonNumberValue::cast_ref(value.syntax()) - .ok_or_else(|| { - DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "number", - value.range(), - ) - }) - .ok()?; - let value = value.value_token().ok()?; - - let result = value.text_trimmed().parse::().map_err(|err| { - emit_diagnostic_form_number( - err, - value.text_trimmed(), - value.text_trimmed_range(), - maximum, - ) - }); - match result { - Ok(number) => Some(number), - Err(err) => { - diagnostics.push(err); - None - } - } - } - - /// It attempts to map a [AnyJsonValue] to a [u64]. - /// - /// ## Errors - /// - /// It will fail if: - /// - `value` can't be cast to [JsonNumberValue] - /// - the value of the node can't be parsed to [u64] - fn map_to_u64( - &self, - value: &AnyJsonValue, - name: &str, - maximum: u64, - diagnostics: &mut Vec, - ) -> Option { - let value = JsonNumberValue::cast_ref(value.syntax()).or_else(|| { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "number", - value.range(), - )); - None - })?; - let value = value.value_token().ok()?; - - let result = value.text_trimmed().parse::().map_err(|err| { - emit_diagnostic_form_number( - err, - value.text_trimmed(), - value.text_trimmed_range(), - maximum, - ) - }); - - match result { - Ok(number) => Some(number), - Err(err) => { - diagnostics.push(err); - None - } - } - } - - /// It attempts to cast [AnyJsonValue] to a [bool] - /// - /// ## Errors - /// - /// The function emits a diagnostic if `value` can't be cast to [JsonBooleanValue] - fn map_to_boolean( - &self, - value: &AnyJsonValue, - name: &str, - diagnostics: &mut Vec, - ) -> Option { - JsonBooleanValue::cast_ref(value.syntax()) - .and_then(|value| Some(value.value_token().ok()?.text_trimmed() == "true")) - .or_else(|| { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "boolean", - value.range(), - )); - - None - }) - } - - /// It attempts to map a [AnyJsonValue] to a [IndexSet] of [String]. - /// - /// ## Errors - /// - /// The function emit diagnostics if: - /// - `value` can't be cast to [JsonArrayValue] - /// - any element of the of the array can't be cast to [JsonStringValue] - fn map_to_index_set_string( - &self, - value: &AnyJsonValue, - name: &str, - diagnostics: &mut Vec, - ) -> Option> { - let array = JsonArrayValue::cast_ref(value.syntax()).or_else(|| { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "array", - value.range(), - )); - None - })?; - let mut elements = IndexSet::new(); - if array.elements().is_empty() { - return None; - } - for element in array.elements() { - let element = element.ok()?; - match element { - AnyJsonValue::JsonStringValue(value) => { - elements.insert(value.inner_string_text().ok()?.to_string()); - } - _ => { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type( - "string", - element.range(), - )); - } - } - } - - Some(elements) - } - - /// It attempts to map a [AnyJsonValue] to a [Vec] of [String]. - /// - /// ## Errors - /// - /// The function emit diagnostics if: - /// - `value` can't be cast to [JsonArrayValue] - /// - any element of the of the array can't be cast to [JsonStringValue] - fn map_to_array_of_strings( - &self, - value: &AnyJsonValue, - name: &str, - diagnostics: &mut Vec, - ) -> Option> { - let array = JsonArrayValue::cast_ref(value.syntax()).or_else(|| { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "array", - value.range(), - )); - None - })?; - let mut elements = Vec::new(); - if array.elements().is_empty() { - return None; - } - for element in array.elements() { - let element = element.ok()?; - match element { - AnyJsonValue::JsonStringValue(value) => { - elements.push(value.inner_string_text().ok()?.to_string()); - } - _ => { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type( - "string", - element.range(), - )); - } - } - } - - Some(elements) - } - - /// It attempts to map [AnyJsonValue] to a generic map. - /// - /// Use this function when the value of your member is another object, and this object - /// needs to be mapped to another type. - /// - /// This function will loop though the list of elements and call [visit_map] on each pair - /// of `name` and `value`. - /// - /// ## Errors - /// This function will emit diagnostics if: - /// - the `value` can't be cast to [JsonObjectValue] - fn map_to_object( - &mut self, - value: &AnyJsonValue, - name: &str, - diagnostics: &mut Vec, - ) -> Option<()> { - let value = JsonObjectValue::cast_ref(value.syntax()).or_else(|| { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "object", - value.range(), - )); - None - })?; - for element in value.json_member_list() { - let element = element.ok()?; - self.visit_map( - element.name().ok()?.syntax(), - element.value().ok()?.syntax(), - diagnostics, - )?; - } - Some(()) - } - - fn map_to_array( - &mut self, - value: &AnyJsonValue, - name: &str, - diagnostics: &mut Vec, - ) -> Option<()> { - let array = JsonArrayValue::cast_ref(value.syntax()).or_else(|| { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "array", - value.range(), - )); - None - })?; - if array.elements().is_empty() { - return None; - } - for element in array.elements() { - let element = element.ok()?; - self.visit_array_member(element.syntax(), diagnostics); - } - - Some(()) - } -} - -impl> VisitJsonNode for V {} - -fn emit_diagnostic_form_number( - parse_error: ParseIntError, - value_text: &str, - value_range: TextRange, - maximum: impl biome_console::fmt::Display, -) -> DeserializationDiagnostic { - let diagnostic = - DeserializationDiagnostic::new(parse_error.to_string()).with_range(value_range); - if value_text.starts_with('-') { - diagnostic.with_note(markup! {"Value can't be negative"}) - } else { - diagnostic.with_note(markup! {"Maximum value accepted is "{{maximum}}}) - } -} - -/// Convenient function to report that `key` is not allowed and -/// to suggest a key in `allowed_keys`. -pub fn report_unknown_map_key( - key: &JsonMemberName, - allowed_keys: &[&str], - diagnostics: &mut Vec, -) { - if let Ok(name) = key.inner_string_text() { - diagnostics.push(DeserializationDiagnostic::new_unknown_key( - name.text(), - key.range(), - allowed_keys, - )) - } -} - -/// Convenient function to report that `variant` is not allowed and -/// to suggest a variant in `allowed_variant`. -pub fn report_unknown_variant( - variant: &JsonStringValue, - allowed_variants: &[&str], - diagnostics: &mut Vec, -) { - if let Ok(value) = variant.inner_string_text() { - diagnostics.push(DeserializationDiagnostic::new_unknown_value( - value.text(), - variant.range(), - allowed_variants, - )); - } -} +use biome_json_syntax::{AnyJsonValue, JsonMemberName, JsonRoot, T}; +use biome_rowan::{AstNode, AstSeparatedList}; /// It attempts to parse and deserialize a source file in JSON. Diagnostics from the parse phase /// are consumed and joined with the diagnostics emitted during the deserialization. /// -/// The data structure that needs to be deserialized needs to implement three important traits: -/// - [Default], to create a first instance of the data structure; -/// - [JsonDeserialize], a trait to begin the deserialization from JSON AST; -/// - [VisitNode], to visit values inside a JSON file; -/// - [VisitJsonNode], to inherit a series of useful functions to handle specifically -/// JSON values; +/// The data structures that need to be deserialized have to implement the [Deserializable] trait. +/// To implement [Deserializable], it can need to implement [DeserializationVisitor] that allows +/// visiting a value. /// /// ## Examples /// /// ``` -/// use biome_deserialize::{DeserializationDiagnostic, VisitNode, Deserialized}; +/// use biome_deserialize::{DeserializationDiagnostic, Deserializable, DeserializableValue, DeserializationVisitor, ExpectedType}; /// use biome_deserialize::json::deserialize_from_json_str; -/// use biome_deserialize::json::{report_unknown_map_key, JsonDeserialize, VisitJsonNode}; -/// use biome_json_syntax::{JsonLanguage, JsonSyntaxNode}; -/// use biome_json_syntax::JsonRoot; -/// use biome_rowan::AstNode; +/// use biome_rowan::{TextRange, TokenText}; /// /// #[derive(Default, Debug, Eq, PartialEq)] /// struct NewConfiguration { -/// lorem: bool +/// lorem: String /// } /// -/// impl VisitNode for NewConfiguration { -/// fn visit_map(&mut self, key: &JsonSyntaxNode, value: &JsonSyntaxNode, diagnostics: &mut Vec) -> Option<()> { -/// let (key, value) = self.get_key_and_value(key, value)?; -/// let name_text = key.inner_string_text().ok()?; -/// let name_text = name_text.text(); -/// match name_text { -/// "lorem" => { -/// self.lorem = self.map_to_boolean(&value, name_text, diagnostics)? -/// } -/// _ => { -/// report_unknown_map_key(&key, &["lorem"], diagnostics); -/// } -/// } -/// Some(()) +/// impl Deserializable for NewConfiguration { +/// fn deserialize( +/// value: impl DeserializableValue, +/// diagnostics: &mut Vec, +/// ) -> Option { +/// value.deserialize(Visitor, diagnostics) /// } /// } /// -/// impl NewConfiguration { -/// fn parse(root: JsonRoot) -> Deserialized { -/// use biome_deserialize::Deserialized; -/// let mut output = Self::default(); -/// let mut diagnostics = vec![]; -/// NewConfiguration::deserialize_from_ast(&root, &mut output, &mut diagnostics); -/// Deserialized::new(output, diagnostics) -/// } -/// } +/// struct Visitor; +/// impl DeserializationVisitor for Visitor { +/// type Output = NewConfiguration; /// +/// const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; /// -/// impl JsonDeserialize for NewConfiguration { -/// fn deserialize_from_ast(root: &JsonRoot, visitor: &mut impl VisitJsonNode, diagnostics: &mut Vec) -> Option<()> { -/// let object = root.value().ok()?; -/// let object = object.as_json_object_value()?; -/// for member in object.json_member_list() { -/// let member = member.ok()?; -/// visitor.visit_map(member.name().ok()?.syntax(), member.value().ok()?.syntax(), diagnostics)?; +/// fn visit_map( +/// self, +/// members: impl Iterator, +/// _range: TextRange, +/// diagnostics: &mut Vec, +/// ) -> Option { +/// const ALLOWED_KEYS: &[&str] = &["strictCase", "enumMemberCase"]; +/// let mut result = NewConfiguration::default(); +/// for (key, value) in members { +/// let key_range = key.range(); +/// let Some(key) = TokenText::deserialize(key, diagnostics) else { +/// continue; +/// }; +/// match key.text() { +/// "lorem" => { +/// if let Some(value) = Deserializable::deserialize(value, diagnostics) { +/// result.lorem = value; +/// } +/// }, +/// _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( +/// key.text(), +/// key_range, +/// ALLOWED_KEYS, +/// )), +/// } /// } -/// Some(()) +/// Some(result) /// } /// } /// /// use biome_json_parser::JsonParserOptions; -/// # fn main() -> Result<(), DeserializationDiagnostic> { -/// let source = r#"{ "lorem": true }"#; -/// let deserialized = deserialize_from_json_str::(&source, JsonParserOptions::default()); -/// assert!(!deserialized.has_errors()); -/// assert_eq!(deserialized.into_deserialized(), NewConfiguration { lorem: true }); -/// # Ok(()) -/// # } -/// -/// +/// let source = r#"{ "lorem": "ipsum" }"#; +/// let deserialized = deserialize_from_json_str::(&source, JsonParserOptions::default()); +/// assert!(!deserialized.has_errors()); +/// assert_eq!(deserialized.into_deserialized().unwrap(), NewConfiguration { lorem: "ipsum".to_string() }); /// ``` -pub fn deserialize_from_json_str( +pub fn deserialize_from_json_str( source: &str, options: JsonParserOptions, -) -> Deserialized -where - Output: Default + VisitJsonNode + JsonDeserialize, -{ - let mut output = Output::default(); - let mut diagnostics = vec![]; +) -> Deserialized { let parse = parse_json(source, options); - - Output::deserialize_from_ast(&parse.tree(), &mut output, &mut diagnostics); + let Deserialized { + diagnostics, + deserialized, + } = deserialize_from_json_ast::(&parse.tree()); let mut errors = parse .into_diagnostics() .into_iter() @@ -582,34 +99,82 @@ where ); Deserialized { diagnostics: errors, - deserialized: output, + deserialized, } } /// Attempts to deserialize a JSON AST, given the `Output`. -pub fn deserialize_from_json_ast(parse: &JsonRoot) -> Deserialized -where - Output: Default + VisitJsonNode + JsonDeserialize, -{ - let mut output = Output::default(); +pub fn deserialize_from_json_ast(parse: &JsonRoot) -> Deserialized { let mut diagnostics = vec![]; - Output::deserialize_from_ast(parse, &mut output, &mut diagnostics); + let deserialized = parse + .value() + .ok() + .and_then(|value| Output::deserialize(value, &mut diagnostics)); Deserialized { diagnostics: diagnostics.into_iter().map(Error::from).collect::>(), - deserialized: output, + deserialized, } } -/// Attempts to deserialize a JSON AST, given the `Output`. -pub fn deserialize_from_json_root(parse: &JsonRoot) -> Deserialized -where - Output: Default + VisitJsonNode + JsonDeserialize, -{ - let mut output = Output::default(); - let mut diagnostics = vec![]; - Output::deserialize_from_ast(parse, &mut output, &mut diagnostics); - Deserialized { - diagnostics: diagnostics.into_iter().map(Error::from).collect::>(), - deserialized: output, +impl DeserializableValue for AnyJsonValue { + fn range(&self) -> biome_rowan::TextRange { + AstNode::range(self) + } + + fn deserialize( + self, + visitor: V, + diagnostics: &mut Vec, + ) -> Option { + let range = AstNode::range(&self); + match self { + AnyJsonValue::JsonArrayValue(array) => { + let items = array.elements().iter().filter_map(|x| x.ok()); + visitor.visit_array(items, range, diagnostics) + } + AnyJsonValue::JsonBogusValue(_) => { + // The parser should emit an error about this node + // No need to emit another diagnostic. + None + } + AnyJsonValue::JsonBooleanValue(value) => { + let value = value.value_token().ok()?; + visitor.visit_bool(value.kind() == T![true], range, diagnostics) + } + AnyJsonValue::JsonNullValue(_) => visitor.visit_null(range, diagnostics), + AnyJsonValue::JsonNumberValue(value) => { + let value = value.value_token().ok()?; + let token_text = value.token_text_trimmed(); + visitor.visit_number(TokenNumber(token_text), range, diagnostics) + } + AnyJsonValue::JsonObjectValue(object) => { + let members = object + .json_member_list() + .iter() + .filter_map(|x| x.ok()) + .filter_map(|x| Some((x.name().ok()?, x.value().ok()?))); + visitor.visit_map(members, range, diagnostics) + } + AnyJsonValue::JsonStringValue(value) => { + let value = value.inner_string_text().ok()?; + visitor.visit_str(value, range, diagnostics) + } + } + } +} + +impl DeserializableValue for JsonMemberName { + fn range(&self) -> biome_rowan::TextRange { + AstNode::range(self) + } + + fn deserialize( + self, + visitor: V, + diagnostics: &mut Vec, + ) -> Option { + let range = AstNode::range(&self); + let value = self.inner_string_text().ok()?; + visitor.visit_str(value, range, diagnostics) } } diff --git a/crates/biome_deserialize/src/lib.rs b/crates/biome_deserialize/src/lib.rs index 25d9d779bda6..514d8a5d48c1 100644 --- a/crates/biome_deserialize/src/lib.rs +++ b/crates/biome_deserialize/src/lib.rs @@ -1,31 +1,346 @@ +//! `biome_deserialize` provides a framework to deserialize textual data format. +//! +//! This is inspired by serde. +//! 0ne of the main difference is the fault-tolerant behavior of `biome_deserialize`. +//! Serde uses a fast-fail strategy, while `biome_deserialize` deserialize as much as possible +//! and report several diagnostics (errors, warning, deprecation messages, ...). +//! +//! The two most important traits are [Deserializable] and [DeserializableValue]. +//! +//! - A type that implements `Deserializable` is a data structure that can be +//! deserialized from any supported data format +//! - A type that implements `DeserializableValue` is a data format that can +//! deserialize any supported data structure. mod diagnostics; -mod visitor; - +mod impls; pub mod json; pub mod string_set; - use biome_diagnostics::{Error, Severity}; -pub use diagnostics::{DeserializationAdvice, DeserializationDiagnostic}; +use biome_rowan::{TextRange, TokenText}; +pub use diagnostics::{DeserializationAdvice, DeserializationDiagnostic, ExpectedType}; +pub use impls::*; use std::fmt::Debug; -pub use string_set::{deserialize_string_set, serialize_string_set, StringSet}; -pub use visitor::VisitNode; +pub use string_set::StringSet; + +/// Implemented by data structures that can deserialize any [DeserializableValue]. +/// +/// `biome_deserialize` provides [Deserializable] implementations for common Rust types. +/// To implement [Deserializable], you can reuse a type that implements [Deserializable] and +/// turn the obtained value into what you want. +/// +/// When deserializing more complex types, such as `struct`, +/// you have to use a type that implements [DeserializationVisitor]. +/// +/// ### Example +/// +/// ``` +/// use biome_deserialize::{DeserializationDiagnostic, Deserializable, DeserializableValue, DeserializationVisitor, ExpectedType}; +/// use biome_deserialize::json::deserialize_from_json_str; +/// use biome_rowan::{TextRange, TokenText}; +/// +/// pub enum Variant { +/// A, +/// B, +/// } +/// +/// impl Deserializable for Variant { +/// fn deserialize( +/// value: impl DeserializableValue, +/// diagnostics: &mut Vec, +/// ) -> Option { +/// const ALLOWED_VARIANTS: &[&str] = &["A", "B", "C"]; +/// let range = value.range(); +/// let value = TokenText::deserialize(value, diagnostics)?; +/// match value.text() { +/// "A" => Some(Variant::A), +/// "B" => Some(Variant::B), +/// _ => { +/// diagnostics.push(DeserializationDiagnostic::new_unknown_value( +/// value.text(), +/// range, +/// ALLOWED_VARIANTS, +/// )); +/// None +/// } +/// } +/// } +/// } +/// ``` +pub trait Deserializable: Sized { + /// Returns the deserialized form of `value`, or `None` if it failed. + /// Any diagnostics emitted during deserialization are appended to `diagnostics`. + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option; +} + +/// Implemented by data structure that can be deserialized. +/// +/// This trait should only be implemented when adding the support for a new data format. +/// See [biome_deserialize::json] for an example of implementation. +pub trait DeserializableValue: Sized { + /// Range in the source content of this value + fn range(&self) -> TextRange; + + /// Returns the deserialized form of this value using `visitor`. + /// Any diagnostics emitted during deserialization are appended to `diagnostics`. + fn deserialize( + self, + visitor: V, + diagnostics: &mut Vec, + ) -> Option; +} + +/// This trait represents a visitor that walks through a [DeserializableValue]. +/// +/// We assume that a deserializable value has one of the following type: +/// - null / none +/// - boolean +/// - number (integer, floats) +/// - string +/// - array +/// - map (key-value pairs with value's type that can depend on its key) +/// +/// Every type is associated to a `visit_` method. +/// [DeserializableValue::deserialize] calls the `viist_` method that matches the type of the value. +/// +/// Most of the time you should implement [Deserializable] and rely on existing deserializable types. +/// You should use a visitor only when you deserialize a map or a union of several types. +/// +/// ### Examples +/// +/// ``` +/// use biome_deserialize::{DeserializationDiagnostic, Deserializable, DeserializableValue, DeserializationVisitor, ExpectedType}; +/// use biome_deserialize::json::deserialize_from_json_str; +/// use biome_rowan::{TextRange, TokenText}; +/// +/// struct Person { +/// name: String +/// } +/// +/// impl Deserializable for Person { +/// fn deserialize( +/// value: impl DeserializableValue, +/// diagnostics: &mut Vec, +/// ) -> Option { +/// value.deserialize(PersonVisitor, diagnostics) +/// } +/// } +/// +/// struct PersonVisitor; +/// impl DeserializationVisitor for PersonVisitor { +/// type Output = Person; +/// const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; +/// +/// fn visit_map( +/// self, +/// members: impl Iterator, +/// range: TextRange, +/// diagnostics: &mut Vec, +/// ) -> Option { +/// const ALLOWED_KEYS: &[&str] = &["name"]; +/// let mut name = None; +/// for (key, value) in members { +/// let key_range = key.range(); +/// let Some(key) = TokenText::deserialize(key, diagnostics) else { +/// continue; +/// }; +/// match key.text() { +/// "name" => { +/// name = Deserializable::deserialize(value, diagnostics); +/// }, +/// _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( +/// key.text(), +/// key_range, +/// ALLOWED_KEYS, +/// )), +/// } +/// } +/// Some(Person { name: name? }) +/// } +/// } +/// +/// enum Union { +/// Bool(bool), +/// Str(String), +/// } +/// +/// impl Deserializable for Union { +/// fn deserialize( +/// value: impl DeserializableValue, +/// diagnostics: &mut Vec, +/// ) -> Option { +/// value.deserialize(UnionVisitor, diagnostics) +/// } +/// } +/// +/// struct UnionVisitor; +/// impl DeserializationVisitor for UnionVisitor { +/// type Output = Union; +/// const EXPECTED_TYPE: ExpectedType = ExpectedType::BOOL.union(ExpectedType::STR); +/// +/// fn visit_bool( +/// self, +/// value: bool, +/// range: TextRange, +/// diagnostics: &mut Vec, +/// ) -> Option { +/// Some(Union::Bool(value)) +/// } +/// +/// fn visit_str( +/// self, +/// value: TokenText, +/// range: TextRange, +/// diagnostics: &mut Vec, +/// ) -> Option { +/// Some(Union::Str(value.text().to_string())) +/// } +/// } +/// ``` +pub trait DeserializationVisitor: Sized { + /// The type of the deserialized form of the visited value. + type Output; + + /// The expected type of the visited value. + const EXPECTED_TYPE: ExpectedType; + + /// The visited value is `null`. + /// + /// The default implementation appends an incorrect type diagnostic to `diagnostics`. + /// The expected type is retrieved from [Self::EXPECTED_TYPE]. + fn visit_null( + self, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + debug_assert!( + !Self::EXPECTED_TYPE.contains(ExpectedType::NULL), + "This method should be implemented because the type is expected." + ); + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + Self::EXPECTED_TYPE, + range, + )); + None + } + + /// The visited value is a `bool`. + /// + /// The default implementation appends an incorrect type diagnostic to `diagnostics`. + /// The expected type is retrieved from [Self::EXPECTED_TYPE]. + fn visit_bool( + self, + _value: bool, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + debug_assert!( + !Self::EXPECTED_TYPE.contains(ExpectedType::BOOL), + "This method should be implemented because the type is expected." + ); + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + Self::EXPECTED_TYPE, + range, + )); + None + } + + /// The visited value is a number (integer or float). + /// The number is represented by a string. + /// + /// The default implementation appends an incorrect type diagnostic to `diagnostics`. + /// The expected type is retrieved from [Self::EXPECTED_TYPE]. + fn visit_number( + self, + _value: TokenNumber, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + debug_assert!( + !Self::EXPECTED_TYPE.contains(ExpectedType::NUMBER), + "This method should be implemented because the type is expected." + ); + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + Self::EXPECTED_TYPE, + range, + )); + None + } + + /// The visited value is a `string`. + /// + /// The default implementation appends an incorrect type diagnostic to `diagnostics`. + /// The expected type is retrieved from [Self::EXPECTED_TYPE]. + fn visit_str( + self, + _value: TokenText, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + debug_assert!( + !Self::EXPECTED_TYPE.contains(ExpectedType::STR), + "This method should be implemented because the type is expected." + ); + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + Self::EXPECTED_TYPE, + range, + )); + None + } + + /// The visited value is an array-like (array, list, vector) structure. + /// + /// The default implementation appends an incorrect type diagnostic to `diagnostics`. + /// The expected type is retrieved from [Self::EXPECTED_TYPE]. + fn visit_array( + self, + _items: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + debug_assert!( + !Self::EXPECTED_TYPE.contains(ExpectedType::ARRAY), + "This method should be implemented because the type is expected." + ); + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + Self::EXPECTED_TYPE, + range, + )); + None + } + + /// The visited value is a `map` (key-value pairs). + /// + /// The default implementation appends an incorrect type diagnostic to `diagnostics`. + /// The expected type is retrieved from [Self::EXPECTED_TYPE]. + fn visit_map( + self, + _members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + debug_assert!( + !Self::EXPECTED_TYPE.contains(ExpectedType::MAP), + "This method should be implemented because the type is expected." + ); + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + Self::EXPECTED_TYPE, + range, + )); + None + } +} /// A small type to interrogate the result of a JSON deserialization #[derive(Debug, Default)] pub struct Deserialized

{ diagnostics: Vec, - deserialized: P, + deserialized: Option

, } impl

Deserialized

{ - /// [DeserializationDiagnostic] are converted into [Error] - pub fn new(deserialized: P, diagnostics: Vec) -> Self { - Self { - deserialized, - diagnostics: diagnostics.into_iter().map(Error::from).collect(), - } - } - /// Consumes self to return the diagnostics pub fn into_diagnostics(self) -> Vec { self.diagnostics @@ -36,7 +351,7 @@ impl

Deserialized

{ } /// Consumes self and returns the deserialized result - pub fn into_deserialized(self) -> P { + pub fn into_deserialized(self) -> Option

{ self.deserialized } @@ -47,7 +362,7 @@ impl

Deserialized

{ } /// Consume itself to return the parsed result and its diagnostics - pub fn consume(self) -> (P, Vec) { + pub fn consume(self) -> (Option

, Vec) { (self.deserialized, self.diagnostics) } } diff --git a/crates/biome_deserialize/src/string_set.rs b/crates/biome_deserialize/src/string_set.rs index 1cf8fe5ed058..7cf5c9796e04 100644 --- a/crates/biome_deserialize/src/string_set.rs +++ b/crates/biome_deserialize/src/string_set.rs @@ -1,20 +1,15 @@ +use crate::{Deserializable, DeserializableValue}; use indexmap::IndexSet; use serde::de::{SeqAccess, Visitor}; use serde::ser::SerializeSeq; use serde::{Deserialize, Serialize}; -use std::marker::PhantomData; use std::str::FromStr; -#[derive(Default, Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +// To implement serde's traits, we encapsulate `IndexSet` in a new type `StringSet`. + +#[derive(Default, Debug, Clone, Eq, PartialEq)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct StringSet( - #[serde( - deserialize_with = "crate::deserialize_string_set", - serialize_with = "crate::serialize_string_set" - )] - pub IndexSet, -); +pub struct StringSet(IndexSet); impl StringSet { pub fn new(index_set: IndexSet) -> Self { @@ -26,7 +21,7 @@ impl StringSet { } pub fn is_empty(&self) -> bool { - self.0.len() == 0 + self.0.is_empty() } pub fn index_set(&self) -> &IndexSet { @@ -42,71 +37,67 @@ impl StringSet { } } -/// Some documentation -pub fn deserialize_string_set<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::de::Deserializer<'de>, -{ - struct IndexVisitor { - marker: PhantomData IndexSet>, - } +impl FromStr for StringSet { + type Err = &'static str; - impl IndexVisitor { - fn new() -> Self { - IndexVisitor { - marker: PhantomData, - } - } + fn from_str(_s: &str) -> Result { + Ok(StringSet::default()) } +} - impl<'de> Visitor<'de> for IndexVisitor { - type Value = IndexSet; - - // Format a message stating what data this Visitor expects to receive. - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("expecting a sequence") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let mut index_set = IndexSet::with_capacity(seq.size_hint().unwrap_or(0)); +impl From> for StringSet { + fn from(value: IndexSet) -> Self { + Self::new(value) + } +} - while let Some(value) = seq.next_element()? { - index_set.insert(value); +impl<'de> Deserialize<'de> for StringSet { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct IndexVisitor; + impl<'de> Visitor<'de> for IndexVisitor { + type Value = IndexSet; + + // Format a message stating what data this Visitor expects to receive. + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("expecting a sequence") } - Ok(index_set) + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut index_set = IndexSet::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(value) = seq.next_element()? { + index_set.insert(value); + } + Ok(index_set) + } } + deserializer.deserialize_seq(IndexVisitor).map(StringSet) } - - deserializer.deserialize_seq(IndexVisitor::new()) -} - -pub fn serialize_string_set(string_set: &IndexSet, s: S) -> Result -where - S: serde::ser::Serializer, -{ - let mut sequence = s.serialize_seq(Some(string_set.len()))?; - let iter = string_set.into_iter(); - for global in iter { - sequence.serialize_element(&global)?; - } - - sequence.end() } -impl FromStr for StringSet { - type Err = String; - - fn from_str(_s: &str) -> Result { - Ok(StringSet::default()) +impl Serialize for StringSet { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut sequence = serializer.serialize_seq(Some(self.len()))?; + for item in self.0.iter() { + sequence.serialize_element(&item)?; + } + sequence.end() } } -impl From> for StringSet { - fn from(value: IndexSet) -> Self { - Self::new(value) +impl Deserializable for StringSet { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + Deserializable::deserialize(value, diagnostics).map(StringSet) } } diff --git a/crates/biome_deserialize/src/visitor.rs b/crates/biome_deserialize/src/visitor.rs deleted file mode 100644 index 84d15e459dee..000000000000 --- a/crates/biome_deserialize/src/visitor.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::DeserializationDiagnostic; -use biome_rowan::{Language, SyntaxNode}; - -/// Generic trait to implement when resolving the configuration from a generic language -pub trait VisitNode: Sized { - /// Called when visiting a value that is not a list or a map. - /// A value can be a string, an integer, a boolean. - fn visit_value( - &mut self, - _node: &SyntaxNode, - _diagnostics: &mut Vec, - ) -> Option<()> { - unimplemented!("you should implement visit_value") - } - - /// Called when visiting a list of key-value. - /// - /// The implementor should loop through the list and call this function by passing two nodes, - /// the key/name as first argument, and the value as second argument. - fn visit_map( - &mut self, - _key: &SyntaxNode, - _value: &SyntaxNode, - _diagnostics: &mut Vec, - ) -> Option<()> { - unimplemented!("you should implement visit_map") - } - - /// Called when visiting a list of elements. - /// - /// The implementor should loop through the list and call this function by passing the encountered nodes. - fn visit_array_member( - &mut self, - _element: &SyntaxNode, - _diagnostics: &mut Vec, - ) -> Option<()> { - unimplemented!("you should implement visit_array_member") - } -} diff --git a/crates/biome_formatter/Cargo.toml b/crates/biome_formatter/Cargo.toml index 048ac4dc8119..b7e2bfe01a47 100644 --- a/crates/biome_formatter/Cargo.toml +++ b/crates/biome_formatter/Cargo.toml @@ -15,6 +15,7 @@ version = "0.2.0" [dependencies] biome_console = { workspace = true } +biome_deserialize = { workspace = true } biome_diagnostics = { workspace = true } biome_rowan = { workspace = true } cfg-if = "1.0.0" diff --git a/crates/biome_formatter/src/format_element/document.rs b/crates/biome_formatter/src/format_element/document.rs index ae0fabd8f030..cd7a765c84b8 100644 --- a/crates/biome_formatter/src/format_element/document.rs +++ b/crates/biome_formatter/src/format_element/document.rs @@ -160,7 +160,7 @@ impl FormatOptions for IrFormatOptions { } fn line_width(&self) -> LineWidth { - LineWidth(80) + LineWidth::default() } fn as_print_options(&self) -> PrinterOptions { diff --git a/crates/biome_formatter/src/lib.rs b/crates/biome_formatter/src/lib.rs index afea3a1a15c4..56d5bed1d4f3 100644 --- a/crates/biome_formatter/src/lib.rs +++ b/crates/biome_formatter/src/lib.rs @@ -51,6 +51,9 @@ use crate::format_element::document::Document; use crate::printed_tokens::PrintedTokens; use crate::printer::{Printer, PrinterOptions}; pub use arguments::{Argument, Arguments}; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, TokenNumber, +}; pub use buffer::{ Buffer, BufferExtensions, BufferSnapshot, Inspect, PreambleBuffer, RemoveSoftLinesBuffer, VecBuffer, @@ -161,21 +164,41 @@ impl From for IndentWidth { pub struct LineWidth(u16); impl LineWidth { + /// Minimum allowed value for a valid [LineWidth] + pub const MIN: u16 = 1; /// Maximum allowed value for a valid [LineWidth] pub const MAX: u16 = 320; /// Return the numeric value for this [LineWidth] - pub fn value(&self) -> u16 { + pub fn get(&self) -> u16 { self.0 } } impl Default for LineWidth { fn default() -> Self { + // Safe unwrap because 80 is greater than `0` and i lower than `u16::MAX``. Self(80) } } +impl Deserializable for LineWidth { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenNumber::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + return Some(value); + } + let diagnostic = + DeserializationDiagnostic::new_out_of_bound_integer(Self::MIN, Self::MAX, range); + diagnostics.push(diagnostic); + None + } +} + /// Error type returned when parsing a [LineWidth] from a string fails pub enum ParseLineWidthError { /// The string could not be parsed as a valid [u16] @@ -217,7 +240,7 @@ impl TryFrom for LineWidth { type Error = LineWidthFromIntError; fn try_from(value: u16) -> Result { - if value > 0 && value <= Self::MAX { + if (Self::MIN..=Self::MAX).contains(&value) { Ok(Self(value)) } else { Err(LineWidthFromIntError(value)) @@ -229,8 +252,9 @@ impl std::fmt::Display for LineWidthFromIntError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!( f, - "The line width exceeds the maximum value ({})", - LineWidth::MAX + "The line width should be between {} and {}", + LineWidth::MIN, + LineWidth::MAX, ) } } diff --git a/crates/biome_formatter_test/src/spec.rs b/crates/biome_formatter_test/src/spec.rs index 4446aa827e9c..968e093e043e 100644 --- a/crates/biome_formatter_test/src/spec.rs +++ b/crates/biome_formatter_test/src/spec.rs @@ -200,7 +200,7 @@ where let (output_code, printed) = self.formatted(&parsed, self.options.clone()); - let max_width = self.options.line_width().value() as usize; + let max_width = self.options.line_width().get() as usize; snapshot_builder = snapshot_builder .with_output_and_options( @@ -222,7 +222,7 @@ where for (index, options) in test_options.into_iter().enumerate() { let (output_code, printed) = self.formatted(&parsed, options.clone()); - let max_width = options.line_width().value() as usize; + let max_width = options.line_width().get() as usize; snapshot_builder = snapshot_builder .with_output_and_options( diff --git a/crates/biome_formatter_test/src/test_prettier_snapshot.rs b/crates/biome_formatter_test/src/test_prettier_snapshot.rs index 410e76d80160..d6fa211ee793 100644 --- a/crates/biome_formatter_test/src/test_prettier_snapshot.rs +++ b/crates/biome_formatter_test/src/test_prettier_snapshot.rs @@ -198,7 +198,7 @@ where .with_output(SnapshotOutput::new(&formatted)) .with_errors(&parsed, &self.test_file().parse_input); - let max_width = self.options.line_width().value() as usize; + let max_width = self.options.line_width().get() as usize; builder = builder.with_lines_exceeding_max_width(&formatted, max_width); builder.finish(relative_file_name); diff --git a/crates/biome_js_analyze/src/analyzers/complexity/no_excessive_cognitive_complexity.rs b/crates/biome_js_analyze/src/analyzers/complexity/no_excessive_cognitive_complexity.rs index dc7fa8500b35..72aa849e4301 100644 --- a/crates/biome_js_analyze/src/analyzers/complexity/no_excessive_cognitive_complexity.rs +++ b/crates/biome_js_analyze/src/analyzers/complexity/no_excessive_cognitive_complexity.rs @@ -4,18 +4,17 @@ use biome_analyze::{ }; use biome_console::markup; use biome_deserialize::{ - json::{report_unknown_map_key, VisitJsonNode}, - DeserializationDiagnostic, VisitNode, + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, }; use biome_js_syntax::{ AnyFunctionLike, JsBreakStatement, JsContinueStatement, JsElseClause, JsLanguage, JsLogicalExpression, JsLogicalOperator, }; -use biome_json_syntax::{JsonLanguage, JsonSyntaxNode}; -use biome_rowan::{AstNode, Language, SyntaxNode, TextRange, WalkEvent}; +use biome_rowan::{AstNode, Language, SyntaxNode, TextRange, TokenText, WalkEvent}; use bpaf::Bpaf; use serde::{Deserialize, Serialize}; -use std::str::FromStr; +use std::{num::NonZeroU8, str::FromStr}; #[cfg(feature = "schemars")] use schemars::JsonSchema; @@ -93,7 +92,7 @@ impl Rule for NoExcessiveCognitiveComplexity { fn run(ctx: &RuleContext) -> Self::Signals { let calculated_score = ctx.query().score.calculated_score; - (calculated_score > ctx.options().max_allowed_complexity).then_some(()) + (calculated_score > ctx.options().max_allowed_complexity.get()).then_some(()) } fn diagnostic(ctx: &RuleContext, _: &Self::State) -> Option { @@ -378,13 +377,13 @@ pub struct ComplexityScore { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ComplexityOptions { /// The maximum complexity score that we allow. Anything higher is considered excessive. - pub max_allowed_complexity: u8, + pub max_allowed_complexity: NonZeroU8, } impl Default for ComplexityOptions { fn default() -> Self { Self { - max_allowed_complexity: 15, + max_allowed_complexity: NonZeroU8::new(15).unwrap(), } } } @@ -398,41 +397,47 @@ impl FromStr for ComplexityOptions { } } -impl ComplexityOptions { - const ALLOWED_KEYS: &'static [&'static str] = &["maxAllowedComplexity"]; +impl Deserializable for ComplexityOptions { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(ComplexityOptionsVisitor, diagnostics) + } } -impl VisitNode for ComplexityOptions { +struct ComplexityOptionsVisitor; +impl DeserializationVisitor for ComplexityOptionsVisitor { + type Output = ComplexityOptions; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &JsonSyntaxNode, - value: &JsonSyntaxNode, + self, + members: impl Iterator, + _range: TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - if name_text == "maxAllowedComplexity" { - if let Some(value) = value - .as_json_number_value() - .and_then(|number_value| u8::from_str(&number_value.text()).ok()) - // Don't allow 0 or no code would pass. - // And don't allow MAX_SCORE or we can't detect exceeding it. - .filter(|&number| number > 0 && number < MAX_SCORE) - { - self.max_allowed_complexity = value; - } else { - diagnostics.push( - DeserializationDiagnostic::new(markup! { - "The field ""maxAllowedComplexity" - " must contain an integer between 1 and "{MAX_SCORE - 1} - }) - .with_range(value.range()), - ); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["maxAllowedComplexity"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "maxAllowedComplexity" => { + if let Some(val) = Deserializable::deserialize(value, diagnostics) { + result.max_allowed_complexity = val; + } + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } - } else { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); } - Some(()) + Some(result) } } diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index 934a539888a4..f98cdc1ba0dc 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -14,8 +14,7 @@ use crate::semantic_analyzers::style::use_naming_convention::{ }; use biome_analyze::options::RuleOptions; use biome_analyze::RuleKey; -use biome_deserialize::{DeserializationDiagnostic, VisitNode}; -use biome_json_syntax::JsonLanguage; +use biome_deserialize::{Deserializable, DeserializableValue, DeserializationDiagnostic}; use bpaf::Bpaf; #[cfg(feature = "schemars")] use schemars::JsonSchema; @@ -46,19 +45,23 @@ impl FromStr for PossibleOptions { } impl PossibleOptions { - pub fn new_from_rule_name(rule_name: &str) -> Option { + pub fn deserialize_from_rule_name( + rule_name: &str, + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { match rule_name { "noExcessiveCognitiveComplexity" => { - Some(Self::Complexity(ComplexityOptions::default())) + Deserializable::deserialize(value, diagnostics).map(Self::Complexity) } "noRestrictedGlobals" => { - Some(Self::RestrictedGlobals(RestrictedGlobalsOptions::default())) + Deserializable::deserialize(value, diagnostics).map(Self::RestrictedGlobals) } "useExhaustiveDependencies" | "useHookAtTopLevel" => { - Some(Self::Hooks(HooksOptions::default())) + Deserializable::deserialize(value, diagnostics).map(Self::Hooks) } "useNamingConvention" => { - Some(Self::NamingConvention(NamingConventionOptions::default())) + Deserializable::deserialize(value, diagnostics).map(Self::NamingConvention) } _ => None, } @@ -99,28 +102,3 @@ impl PossibleOptions { } } } - -impl VisitNode for PossibleOptions { - fn visit_map( - &mut self, - key: &biome_rowan::SyntaxNode, - value: &biome_rowan::SyntaxNode, - diagnostics: &mut Vec, - ) -> Option<()> { - match self { - PossibleOptions::Complexity(options) => { - options.visit_map(key, value, diagnostics)?; - } - PossibleOptions::Hooks(options) => { - options.visit_map(key, value, diagnostics)?; - } - PossibleOptions::NamingConvention(options) => { - options.visit_map(key, value, diagnostics)?; - } - PossibleOptions::RestrictedGlobals(options) => { - options.visit_map(key, value, diagnostics)?; - } - } - Some(()) - } -} diff --git a/crates/biome_js_analyze/src/semantic_analyzers/correctness/use_exhaustive_dependencies.rs b/crates/biome_js_analyze/src/semantic_analyzers/correctness/use_exhaustive_dependencies.rs index 17fc201b5ddf..90e6394ea74c 100644 --- a/crates/biome_js_analyze/src/semantic_analyzers/correctness/use_exhaustive_dependencies.rs +++ b/crates/biome_js_analyze/src/semantic_analyzers/correctness/use_exhaustive_dependencies.rs @@ -2,15 +2,16 @@ use crate::react::hooks::*; use crate::semantic_services::Semantic; use biome_analyze::{context::RuleContext, declare_rule, Rule, RuleDiagnostic}; use biome_console::markup; -use biome_deserialize::json::{report_unknown_map_key, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, VisitNode}; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; use biome_js_semantic::{Capture, SemanticModel}; use biome_js_syntax::{ binding_ext::AnyJsBindingDeclaration, JsCallExpression, JsStaticMemberExpression, JsSyntaxKind, JsSyntaxNode, JsVariableDeclaration, TextRange, }; -use biome_json_syntax::{JsonLanguage, JsonSyntaxNode}; -use biome_rowan::{AstNode, AstSeparatedList, SyntaxNodeCast}; +use biome_rowan::{AstNode, SyntaxNodeCast, TokenText}; use bpaf::Bpaf; use rustc_hash::{FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; @@ -217,10 +218,6 @@ pub struct HooksOptions { pub hooks: Vec, } -impl HooksOptions { - const ALLOWED_KEYS: &'static [&'static str] = &["hooks"]; -} - impl FromStr for HooksOptions { type Err = (); @@ -229,6 +226,57 @@ impl FromStr for HooksOptions { } } +impl Deserializable for HooksOptions { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(HooksOptionsVisitor, diagnostics) + } +} + +struct HooksOptionsVisitor; +impl DeserializationVisitor for HooksOptionsVisitor { + type Output = HooksOptions; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + + fn visit_map( + self, + members: impl Iterator, + _range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["hooks"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "hooks" => { + let val_range = value.range(); + result.hooks = + Deserializable::deserialize(value, diagnostics).unwrap_or_default(); + if result.hooks.is_empty() { + diagnostics.push( + DeserializationDiagnostic::new("At least one element is needed") + .with_range(val_range), + ); + } + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), + } + } + Some(result) + } +} + #[derive(Default, Deserialize, Serialize, Eq, PartialEq, Debug, Clone, Bpaf)] #[cfg_attr(feature = "schemars", derive(JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -246,48 +294,6 @@ pub struct Hooks { pub dependencies_index: Option, } -impl Hooks { - const ALLOWED_KEYS: &'static [&'static str] = &["name", "closureIndex", "dependenciesIndex"]; -} - -impl VisitNode for Hooks { - fn visit_map( - &mut self, - key: &JsonSyntaxNode, - value: &JsonSyntaxNode, - diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "name" => { - self.name = self.map_to_string(&value, name_text, diagnostics)?; - if self.name.is_empty() { - diagnostics.push( - DeserializationDiagnostic::new(markup!( - "The field ""name"" is mandatory" - )) - .with_range(value.range()), - ) - } - } - "closureIndex" => { - self.closure_index = self.map_to_usize(&value, name_text, usize::MAX, diagnostics); - } - "dependenciesIndex" => { - self.dependencies_index = - self.map_to_usize(&value, name_text, usize::MAX, diagnostics); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); - } - } - - Some(()) - } -} - impl FromStr for Hooks { type Err = (); @@ -296,36 +302,62 @@ impl FromStr for Hooks { } } -impl VisitNode for HooksOptions { - fn visit_map( - &mut self, - key: &JsonSyntaxNode, - value: &JsonSyntaxNode, +impl Deserializable for Hooks { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - if name_text == "hooks" { - let array = value.as_json_array_value()?; - if array.elements().len() < 1 { - diagnostics.push( - DeserializationDiagnostic::new("At least one element is needed") - .with_range(array.range()), - ); - return Some(()); - } + ) -> Option { + value.deserialize(HooksVisitor, diagnostics) + } +} + +struct HooksVisitor; +impl DeserializationVisitor for HooksVisitor { + type Output = Hooks; - for element in array.elements() { - let element = element.ok()?; - let mut hooks = Hooks::default(); - hooks.map_to_object(&element, "hooks", diagnostics)?; - self.hooks.push(hooks); + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + + fn visit_map( + self, + members: impl Iterator, + _range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["name", "closureIndex", "dependenciesIndex"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "name" => { + let val_range = value.range(); + result.name = + Deserializable::deserialize(value, diagnostics).unwrap_or_default(); + if result.name.is_empty() { + diagnostics.push( + DeserializationDiagnostic::new(markup!( + "The field ""name"" is mandatory" + )) + .with_range(val_range), + ) + } + } + "closureIndex" => { + result.closure_index = Deserializable::deserialize(value, diagnostics); + } + "dependenciesIndex" => { + result.dependencies_index = Deserializable::deserialize(value, diagnostics); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } - } else { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); } - Some(()) + Some(result) } } diff --git a/crates/biome_js_analyze/src/semantic_analyzers/style/no_restricted_globals.rs b/crates/biome_js_analyze/src/semantic_analyzers/style/no_restricted_globals.rs index ffe6ad20a5fa..63f6041da0de 100644 --- a/crates/biome_js_analyze/src/semantic_analyzers/style/no_restricted_globals.rs +++ b/crates/biome_js_analyze/src/semantic_analyzers/style/no_restricted_globals.rs @@ -2,12 +2,13 @@ use crate::semantic_services::SemanticServices; use biome_analyze::context::RuleContext; use biome_analyze::{declare_rule, Rule, RuleDiagnostic}; use biome_console::markup; -use biome_deserialize::json::{report_unknown_map_key, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, VisitNode}; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; use biome_js_semantic::{Binding, BindingExtensions}; use biome_js_syntax::{AnyJsIdentifierUsage, TextRange}; -use biome_json_syntax::JsonLanguage; -use biome_rowan::{AstNode, SyntaxNode}; +use biome_rowan::{AstNode, TokenText}; use bpaf::Bpaf; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -69,10 +70,6 @@ pub struct RestrictedGlobalsOptions { denied_globals: Option>, } -impl RestrictedGlobalsOptions { - const ALLOWED_KEYS: &'static [&'static str] = &["deniedGlobals"]; -} - // Required by [Bpaf]. impl FromStr for RestrictedGlobalsOptions { type Err = &'static str; @@ -83,23 +80,46 @@ impl FromStr for RestrictedGlobalsOptions { } } -impl VisitNode for RestrictedGlobalsOptions { +impl Deserializable for RestrictedGlobalsOptions { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(RestrictedGlobalsOptionsVisitor, diagnostics) + } +} + +struct RestrictedGlobalsOptionsVisitor; +impl DeserializationVisitor for RestrictedGlobalsOptionsVisitor { + type Output = RestrictedGlobalsOptions; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - if name_text == "deniedGlobals" { - self.denied_globals = self.map_to_array_of_strings(&value, name_text, diagnostics); - } else { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["deniedGlobals"]; + let mut denied_globals = None; + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "deniedGlobals" => { + denied_globals = Deserializable::deserialize(value, diagnostics); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), + } } - - Some(()) + Some(Self::Output { denied_globals }) } } diff --git a/crates/biome_js_analyze/src/semantic_analyzers/style/use_naming_convention.rs b/crates/biome_js_analyze/src/semantic_analyzers/style/use_naming_convention.rs index caffcbc3b85d..e0a3acb12d3d 100644 --- a/crates/biome_js_analyze/src/semantic_analyzers/style/use_naming_convention.rs +++ b/crates/biome_js_analyze/src/semantic_analyzers/style/use_naming_convention.rs @@ -12,8 +12,8 @@ use biome_analyze::{ }; use biome_console::markup; use biome_deserialize::{ - json::{report_unknown_map_key, report_unknown_variant, VisitJsonNode}, - DeserializationDiagnostic, VisitNode, + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, }; use biome_diagnostics::Applicability; use biome_js_semantic::CanBeImportedExported; @@ -24,9 +24,8 @@ use biome_js_syntax::{ JsVariableDeclarator, JsVariableKind, TsEnumMember, TsIdentifierBinding, TsTypeParameterName, }; use biome_js_unicode_table::is_js_ident; -use biome_json_syntax::{JsonLanguage, JsonStringValue}; use biome_rowan::{ - declare_node_union, AstNode, AstNodeList, BatchMutationExt, SyntaxNode, SyntaxResult, TokenText, + declare_node_union, AstNode, AstNodeList, BatchMutationExt, SyntaxResult, TextRange, TokenText, }; use bpaf::Bpaf; use serde::{Deserialize, Serialize}; @@ -455,10 +454,6 @@ pub struct NamingConventionOptions { pub enum_member_case: EnumMemberCase, } -impl NamingConventionOptions { - const ALLOWED_KEYS: &'static [&'static str] = &["strictCase", "enumMemberCase"]; -} - const fn default_strict_case() -> bool { true } @@ -490,29 +485,53 @@ impl FromStr for NamingConventionOptions { } } -impl VisitNode for NamingConventionOptions { +impl Deserializable for NamingConventionOptions { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(NamingConventionOptionsVisitor, diagnostics) + } +} + +struct NamingConventionOptionsVisitor; +impl DeserializationVisitor for NamingConventionOptionsVisitor { + type Output = NamingConventionOptions; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "strictCase" => { - self.strict_case = self.map_to_boolean(&value, name_text, diagnostics)? - } - "enumMemberCase" => { - self.enum_member_case - .map_to_known_string(&value, name_text, diagnostics)?; - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["strictCase", "enumMemberCase"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "strictCase" => { + if let Some(strict_case) = Deserializable::deserialize(value, diagnostics) { + result.strict_case = strict_case; + } + } + "enumMemberCase" => { + if let Some(case) = Deserializable::deserialize(value, diagnostics) { + result.enum_member_case = case; + } + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } } - Some(()) + Some(result) } } @@ -547,23 +566,24 @@ impl FromStr for EnumMemberCase { } } -impl EnumMemberCase { - const ALLOWED_VARIANTS: &'static [&'static str] = &["camelCase", "CONSTANT_CASE", "PascalCase"]; -} - -impl VisitNode for EnumMemberCase { - fn visit_value( - &mut self, - node: &SyntaxNode, +impl Deserializable for EnumMemberCase { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let node = JsonStringValue::cast_ref(node)?; - if let Ok(value) = node.inner_string_text().ok()?.text().parse::() { - *self = value; + ) -> Option { + const ALLOWED_VARIANTS: &[&str] = &["camelCase", "CONSTANT_CASE", "PascalCase"]; + let range = value.range(); + let value = TokenText::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + Some(value) } else { - report_unknown_variant(&node, Self::ALLOWED_VARIANTS, diagnostics); + diagnostics.push(DeserializationDiagnostic::new_unknown_value( + value.text(), + range, + ALLOWED_VARIANTS, + )); + None } - Some(()) } } diff --git a/crates/biome_js_analyze/tests/specs/complexity/noExcessiveCognitiveComplexity/invalidConfig.js.snap b/crates/biome_js_analyze/tests/specs/complexity/noExcessiveCognitiveComplexity/invalidConfig.js.snap index 60e20ad68a22..4236bbb253fa 100644 --- a/crates/biome_js_analyze/tests/specs/complexity/noExcessiveCognitiveComplexity/invalidConfig.js.snap +++ b/crates/biome_js_analyze/tests/specs/complexity/noExcessiveCognitiveComplexity/invalidConfig.js.snap @@ -20,7 +20,7 @@ function simpleBranches() { ``` invalidConfig.options:9:31 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The field maxAllowedComplexity must contain an integer between 1 and 254 + × The number should be an integer between 1 and 255. 7 │ "level": "error", 8 │ "options": { diff --git a/crates/biome_js_formatter/Cargo.toml b/crates/biome_js_formatter/Cargo.toml index 347d8a17ec65..a80c01c47992 100644 --- a/crates/biome_js_formatter/Cargo.toml +++ b/crates/biome_js_formatter/Cargo.toml @@ -13,6 +13,7 @@ version = "0.2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +biome_console = { workspace = true } biome_deserialize = { workspace = true } biome_diagnostics_categories = { workspace = true } biome_formatter = { workspace = true } diff --git a/crates/biome_js_formatter/src/context.rs b/crates/biome_js_formatter/src/context.rs index 09aa93a88a31..f8792e44bac5 100644 --- a/crates/biome_js_formatter/src/context.rs +++ b/crates/biome_js_formatter/src/context.rs @@ -1,7 +1,6 @@ use crate::comments::{FormatJsLeadingComment, JsCommentStyle, JsComments}; use crate::context::trailing_comma::TrailingComma; -use biome_deserialize::json::report_unknown_variant; -use biome_deserialize::{DeserializationDiagnostic, VisitNode}; +use biome_deserialize::{Deserializable, DeserializableValue, DeserializationDiagnostic}; use biome_formatter::printer::PrinterOptions; use biome_formatter::token::string::Quote; use biome_formatter::{ @@ -9,8 +8,7 @@ use biome_formatter::{ LineWidth, TransformSourceMap, }; use biome_js_syntax::{AnyJsFunctionBody, JsFileSource, JsLanguage}; -use biome_json_syntax::{JsonLanguage, JsonStringValue}; -use biome_rowan::{AstNode, SyntaxNode}; +use biome_rowan::TokenText; use std::fmt; use std::fmt::Debug; use std::rc::Rc; @@ -318,7 +316,7 @@ impl fmt::Display for JsFormatOptions { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "Indent style: {}", self.indent_style)?; writeln!(f, "Indent width: {}", self.indent_width.value())?; - writeln!(f, "Line width: {}", self.line_width.value())?; + writeln!(f, "Line width: {}", self.line_width.get())?; writeln!(f, "Quote style: {}", self.quote_style)?; writeln!(f, "JSX quote style: {}", self.jsx_quote_style)?; writeln!(f, "Quote properties: {}", self.quote_properties)?; @@ -416,23 +414,26 @@ impl From for Quote { } } -impl QuoteStyle { - const ALLOWED_VARIANTS: &'static [&'static str] = &["double", "single"]; -} - -impl VisitNode for QuoteStyle { - fn visit_value( - &mut self, - node: &SyntaxNode, +impl Deserializable for QuoteStyle { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let node = JsonStringValue::cast_ref(node)?; - if let Ok(value) = node.inner_string_text().ok()?.text().parse::() { - *self = value; - } else { - report_unknown_variant(&node, Self::ALLOWED_VARIANTS, diagnostics); + ) -> Option { + const ALLOWED_VARIANTS: &[&str] = &["double", "single"]; + let range = value.range(); + let value = TokenText::deserialize(value, diagnostics)?; + match value.text() { + "double" => Some(QuoteStyle::Double), + "single" => Some(QuoteStyle::Single), + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_value( + value.text(), + range, + ALLOWED_VARIANTS, + )); + None + } } - Some(()) } } @@ -470,23 +471,26 @@ impl fmt::Display for QuoteProperties { } } -impl QuoteProperties { - const ALLOWED_VARIANTS: &'static [&'static str] = &["preserve", "asNeeded"]; -} - -impl VisitNode for QuoteProperties { - fn visit_value( - &mut self, - node: &SyntaxNode, +impl Deserializable for QuoteProperties { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let node = JsonStringValue::cast_ref(node)?; - match node.inner_string_text().ok()?.text() { - "asNeeded" => *self = Self::AsNeeded, - "preserve" => *self = Self::Preserve, - _ => report_unknown_variant(&node, Self::ALLOWED_VARIANTS, diagnostics), + ) -> Option { + const ALLOWED_VARIANTS: &[&str] = &["preserve", "asNeeded"]; + let range = value.range(); + let value = TokenText::deserialize(value, diagnostics)?; + match value.text() { + "asNeeded" => Some(QuoteProperties::AsNeeded), + "preserve" => Some(QuoteProperties::Preserve), + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_value( + value.text(), + range, + ALLOWED_VARIANTS, + )); + None + } } - Some(()) } } @@ -533,23 +537,26 @@ impl fmt::Display for Semicolons { } } -impl Semicolons { - const ALLOWED_VARIANTS: &'static [&'static str] = &["always", "asNeeded"]; -} - -impl VisitNode for Semicolons { - fn visit_value( - &mut self, - node: &SyntaxNode, +impl Deserializable for Semicolons { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let node = JsonStringValue::cast_ref(node)?; - match node.inner_string_text().ok()?.text() { - "asNeeded" => *self = Self::AsNeeded, - "always" => *self = Self::Always, - _ => report_unknown_variant(&node, Self::ALLOWED_VARIANTS, diagnostics), + ) -> Option { + const ALLOWED_VARIANTS: &[&str] = &["always", "asNeeded"]; + let range = value.range(); + let value = TokenText::deserialize(value, diagnostics)?; + match value.text() { + "always" => Some(Semicolons::Always), + "asNeeded" => Some(Semicolons::AsNeeded), + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_value( + value.text(), + range, + ALLOWED_VARIANTS, + )); + None + } } - Some(()) } } @@ -597,22 +604,25 @@ impl fmt::Display for ArrowParentheses { } } -impl ArrowParentheses { - const ALLOWED_VARIANTS: &'static [&'static str] = &["asNeeded", "always"]; -} - -impl VisitNode for ArrowParentheses { - fn visit_value( - &mut self, - node: &SyntaxNode, +impl Deserializable for ArrowParentheses { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let node = JsonStringValue::cast_ref(node)?; - match node.inner_string_text().ok()?.text() { - "asNeeded" => *self = Self::AsNeeded, - "always" => *self = Self::Always, - _ => report_unknown_variant(&node, Self::ALLOWED_VARIANTS, diagnostics), + ) -> Option { + const ALLOWED_VARIANTS: &[&str] = &["asNeeded", "always"]; + let range = value.range(); + let value = TokenText::deserialize(value, diagnostics)?; + match value.text() { + "always" => Some(ArrowParentheses::Always), + "asNeeded" => Some(ArrowParentheses::AsNeeded), + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_value( + value.text(), + range, + ALLOWED_VARIANTS, + )); + None + } } - Some(()) } } diff --git a/crates/biome_js_formatter/src/context/trailing_comma.rs b/crates/biome_js_formatter/src/context/trailing_comma.rs index 743d256162fe..6690832ab291 100644 --- a/crates/biome_js_formatter/src/context/trailing_comma.rs +++ b/crates/biome_js_formatter/src/context/trailing_comma.rs @@ -1,13 +1,10 @@ use crate::prelude::*; use crate::{JsFormatContext, JsFormatOptions}; -use biome_deserialize::json::report_unknown_variant; -use biome_deserialize::{DeserializationDiagnostic, VisitNode}; -use biome_formatter::formatter::Formatter; +use biome_deserialize::{Deserializable, DeserializableValue, DeserializationDiagnostic}; use biome_formatter::prelude::{if_group_breaks, text}; use biome_formatter::write; use biome_formatter::{Format, FormatResult}; -use biome_json_syntax::{JsonLanguage, JsonStringValue}; -use biome_rowan::SyntaxNode; +use biome_rowan::TokenText; use std::fmt; use std::str::FromStr; @@ -107,22 +104,26 @@ impl fmt::Display for TrailingComma { } } -impl TrailingComma { - const ALLOWED_VARIANTS: &'static [&'static str] = &["all", "es5", "none"]; -} - -impl VisitNode for TrailingComma { - fn visit_value( - &mut self, - node: &SyntaxNode, +impl Deserializable for TrailingComma { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let node = JsonStringValue::cast_ref(node)?; - if let Ok(value) = node.inner_string_text().ok()?.text().parse::() { - *self = value; - } else { - report_unknown_variant(&node, Self::ALLOWED_VARIANTS, diagnostics); + ) -> Option { + const ALLOWED_VARIANTS: &[&str] = &["all", "es5", "none"]; + let range = value.range(); + let value = TokenText::deserialize(value, diagnostics)?; + match value.text() { + "all" => Some(TrailingComma::All), + "es5" => Some(TrailingComma::Es5), + "none" => Some(TrailingComma::None), + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_value( + value.text(), + range, + ALLOWED_VARIANTS, + )); + None + } } - Some(()) } } diff --git a/crates/biome_js_formatter/src/utils/assignment_like.rs b/crates/biome_js_formatter/src/utils/assignment_like.rs index d046e111bc03..7226e36b7c9a 100644 --- a/crates/biome_js_formatter/src/utils/assignment_like.rs +++ b/crates/biome_js_formatter/src/utils/assignment_like.rs @@ -1082,7 +1082,7 @@ fn is_poorly_breakable_member_or_call_chain( expression: &AnyJsExpression, f: &Formatter, ) -> SyntaxResult { - let threshold = f.options().line_width().value() / 4; + let threshold = f.options().line_width().get() / 4; // Only call and member chains are poorly breakable // - `obj.member.prop` diff --git a/crates/biome_json_formatter/src/context.rs b/crates/biome_json_formatter/src/context.rs index 436784d72c90..434291d8267e 100644 --- a/crates/biome_json_formatter/src/context.rs +++ b/crates/biome_json_formatter/src/context.rs @@ -122,6 +122,6 @@ impl fmt::Display for JsonFormatOptions { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Indent style: {}", self.indent_style)?; writeln!(f, "Indent width: {}", self.indent_width.value())?; - writeln!(f, "Line width: {}", self.line_width.value()) + writeln!(f, "Line width: {}", self.line_width.get()) } } diff --git a/crates/biome_lsp/src/session.rs b/crates/biome_lsp/src/session.rs index b35db4d0fb3c..c551450e4f2e 100644 --- a/crates/biome_lsp/src/session.rs +++ b/crates/biome_lsp/src/session.rs @@ -408,6 +408,7 @@ impl Session { let status = match load_config(&self.fs, base_path) { Ok(Some(payload)) => { let (configuration, diagnostics) = payload.deserialized.consume(); + let configuration = configuration.unwrap_or_default(); if !diagnostics.is_empty() { warn!("The deserialization of the configuration resulted in errors. Biome will use its defaults where possible."); } diff --git a/crates/biome_project/src/node_js_project/mod.rs b/crates/biome_project/src/node_js_project/mod.rs index 3caa0a200bb6..6a9c32522519 100644 --- a/crates/biome_project/src/node_js_project/mod.rs +++ b/crates/biome_project/src/node_js_project/mod.rs @@ -32,7 +32,7 @@ impl Project for NodeJsProject { fn deserialize_manifest(&mut self, content: &ProjectLanguageRoot) { let manifest = Self::Manifest::deserialize_manifest(content); let (package, deserialize_diagnostics) = manifest.consume(); - self.manifest = package; + self.manifest = package.unwrap_or_default(); self.diagnostics = deserialize_diagnostics; } diff --git a/crates/biome_project/src/node_js_project/package_json.rs b/crates/biome_project/src/node_js_project/package_json.rs index 5880296af97d..0ec6e4697614 100644 --- a/crates/biome_project/src/node_js_project/package_json.rs +++ b/crates/biome_project/src/node_js_project/package_json.rs @@ -1,10 +1,12 @@ use crate::{LanguageRoot, Manifest}; -use biome_deserialize::json::{deserialize_from_json_ast, JsonDeserialize, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, Deserialized, VisitNode}; -use biome_json_syntax::{AnyJsonValue, JsonLanguage, JsonRoot, JsonStringValue, JsonSyntaxNode}; -use biome_rowan::{AstNode, SyntaxNode}; +use biome_deserialize::json::deserialize_from_json_ast; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + Deserialized, ExpectedType, +}; +use biome_json_syntax::JsonLanguage; +use biome_rowan::TokenText; use biome_text_size::{TextRange, TextSize}; -use node_semver::{SemverError, Version}; use rustc_hash::FxHashMap; use std::ops::Add; @@ -30,147 +32,109 @@ impl Manifest for PackageJson { #[derive(Debug, Default)] pub struct Dependencies(FxHashMap); -impl JsonDeserialize for PackageJson { - fn deserialize_from_ast( - root: &JsonRoot, - visitor: &mut impl VisitJsonNode, +#[derive(Debug)] +pub struct Version(node_semver::Version); + +impl Deserializable for PackageJson { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let value = root.value().ok()?; - match value { - AnyJsonValue::JsonObjectValue(node) => { - for element in node.json_member_list() { - let element = element.ok()?; - let member_name = element.name().ok()?; - let member_value = element.value().ok()?; - visitor.visit_map(member_name.syntax(), member_value.syntax(), diagnostics)?; - } - Some(()) - } - _ => { - diagnostics.push( - DeserializationDiagnostic::new("The manifest should be an object") - .with_range(root.range()), - ); - None - } - } + ) -> Option { + value.deserialize(PackageJsonVisitor, diagnostics) } } -impl VisitNode for PackageJson { - fn visit_value( - &mut self, - _node: &JsonSyntaxNode, - _diagnostics: &mut Vec, - ) -> Option<()> { - // each package can add their own field, so we should ignore any extraneous key - // and only deserialize the ones that Rome deems important - Some(()) - } +struct PackageJsonVisitor; +impl DeserializationVisitor for PackageJsonVisitor { + type Output = PackageJson; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; fn visit_map( - &mut self, - key: &JsonSyntaxNode, - value: &JsonSyntaxNode, + self, + members: impl Iterator, + _range: TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "version" => { - let version = self.map_to_string(&value, name_text, diagnostics)?; - self.version = parse_to_version(&version, value.syntax(), diagnostics); - } - "name" => { - self.name = self.map_to_string(&value, name_text, diagnostics); - } - "license" => { - // TODO: add proper parsing of license, e.g. support for AND keywords - self.license = self - .map_to_string(&value, name_text, diagnostics) - .map(|license| (license, value.range())); - } - "description" => { - self.description = self.map_to_string(&value, name_text, diagnostics); - } - "dependencies" => { - let mut dependencies = Dependencies::default(); - dependencies.map_to_object(&value, name_text, diagnostics)?; - self.dependencies = dependencies; - } - "devDependencies" => { - let mut dev_dependencies = Dependencies::default(); - dev_dependencies.map_to_object(&value, name_text, diagnostics)?; - self.dev_dependencies = dev_dependencies; - } - "optionalDependencies" => { - let mut optional_dependencies = Dependencies::default(); - optional_dependencies.map_to_object(&value, name_text, diagnostics)?; - self.optional_dependencies = optional_dependencies; + ) -> Option { + let mut result = Self::Output::default(); + for (key, value) in members { + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "version" => { + result.version = Deserializable::deserialize(value, diagnostics); + } + "name" => { + result.name = Deserializable::deserialize(value, diagnostics); + } + "license" => { + let license_range = value.range(); + // TODO: add proper parsing of license, e.g. support for AND keywords + result.license = Deserializable::deserialize(value, diagnostics) + .map(|license| (license, license_range)); + } + "description" => { + result.description = Deserializable::deserialize(value, diagnostics); + } + "dependencies" => { + if let Some(deps) = Deserializable::deserialize(value, diagnostics) { + result.dependencies = deps; + } + } + "devDependencies" => { + if let Some(deps) = Deserializable::deserialize(value, diagnostics) { + result.dev_dependencies = deps; + } + } + "optionalDependencies" => { + if let Some(deps) = Deserializable::deserialize(value, diagnostics) { + result.optional_dependencies = deps; + } + } + _ => { + // each package can add their own field, so we should ignore any extraneous key + // and only deserialize the ones that Rome deems important + } } - _ => {} } - Some(()) + Some(result) } } -impl VisitNode for Dependencies { - fn visit_value( - &mut self, - _node: &SyntaxNode, - _diagnostics: &mut Vec, - ) -> Option<()> { - Some(()) - } - fn visit_map( - &mut self, - key: &JsonSyntaxNode, - value: &JsonSyntaxNode, +impl Deserializable for Dependencies { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - - let value = JsonStringValue::cast_ref(value.syntax()).or_else(|| { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name_text, - "string", - value.range(), - )); - None - })?; - let version = value.inner_string_text().ok()?; - let version = parse_to_version(version.text(), value.syntax(), diagnostics); - if let Some(version) = version { - self.0.insert(name_text.to_string(), version); - } - - Some(()) + ) -> Option { + Some(Dependencies(Deserializable::deserialize( + value, + diagnostics, + )?)) } } -fn parse_to_version( - version: &str, - value: &JsonSyntaxNode, - diagnostics: &mut Vec, -) -> Option { - let result: Result = version.parse(); - match result { - Ok(version) => Some(version), - Err(err) => { - let (start, end) = err.location(); - let start_range = value.text_trimmed_range().start(); - let end_range = value.text_trimmed_range().end(); - let range = TextRange::new( - start_range.add(TextSize::from(start as u32)), - end_range.add(TextSize::from(end as u32)), - ); - diagnostics - .push(DeserializationDiagnostic::new(err.kind().to_string()).with_range(range)); - None +impl Deserializable for Version { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenText::deserialize(value, diagnostics)?; + match value.parse() { + Ok(version) => Some(Version(version)), + Err(err) => { + let (start, end) = err.location(); + let start_range = range.start(); + let end_range = range.end(); + let range = TextRange::new( + start_range.add(TextSize::from(start as u32)), + end_range.add(TextSize::from(end as u32)), + ); + diagnostics + .push(DeserializationDiagnostic::new(err.kind().to_string()).with_range(range)); + None + } } } } diff --git a/crates/biome_project/tests/invalid/lisence_not_string.json.snap b/crates/biome_project/tests/invalid/lisence_not_string.json.snap index 3b73ca90a5a1..ae86002fc2d8 100644 --- a/crates/biome_project/tests/invalid/lisence_not_string.json.snap +++ b/crates/biome_project/tests/invalid/lisence_not_string.json.snap @@ -4,7 +4,7 @@ expression: lisence_not_string.json --- lisence_not_string.json:2:13 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key license is incorrect. Expected a string. + × Incorrect type, expected a string. 1 │ { > 2 │ "license": false diff --git a/crates/biome_project/tests/invalid/name.json.snap b/crates/biome_project/tests/invalid/name.json.snap index 0a8d340c9de6..2aa400e1c886 100644 --- a/crates/biome_project/tests/invalid/name.json.snap +++ b/crates/biome_project/tests/invalid/name.json.snap @@ -4,7 +4,7 @@ expression: name.json --- name.json:2:10 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key name is incorrect. Expected a string. + × Incorrect type, expected a string. 1 │ { > 2 │ "name": false diff --git a/crates/biome_service/src/configuration/diagnostics.rs b/crates/biome_service/src/configuration/diagnostics.rs index 4bd724ea05fd..690b77f200b8 100644 --- a/crates/biome_service/src/configuration/diagnostics.rs +++ b/crates/biome_service/src/configuration/diagnostics.rs @@ -367,6 +367,7 @@ mod test { }"#; let _result = deserialize_from_json_str::(content, JsonParserOptions::default()) - .into_deserialized(); + .into_deserialized() + .unwrap_or_default(); } } diff --git a/crates/biome_service/src/configuration/formatter.rs b/crates/biome_service/src/configuration/formatter.rs index 6f0fffda8a2d..bd990356fd98 100644 --- a/crates/biome_service/src/configuration/formatter.rs +++ b/crates/biome_service/src/configuration/formatter.rs @@ -205,7 +205,7 @@ pub fn serialize_line_width(line_width: &Option, s: S) -> Result, + ) -> Option { + value.deserialize(ConfigurationVisitor, diagnostics) + } } -impl VisitNode for Configuration { +struct ConfigurationVisitor; +impl DeserializationVisitor for ConfigurationVisitor { + type Output = Configuration; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "$schema" => { - self.schema = self.map_to_string(&value, name_text, diagnostics); - } - "files" => { - let mut files = FilesConfiguration::default(); - files.map_to_object(&value, name_text, diagnostics)?; - self.files = Some(files); - } - "vcs" => { - let mut vcs = VcsConfiguration::default(); - vcs.map_to_object(&value, name_text, diagnostics)?; - validate_vcs_configuration(&value, &mut vcs, diagnostics); - self.vcs = Some(vcs); - } - "formatter" => { - let mut formatter = FormatterConfiguration::default(); - formatter.map_to_object(&value, name_text, diagnostics)?; - self.formatter = Some(formatter); - } - "linter" => { - let mut linter = LinterConfiguration::default(); - linter.map_to_object(&value, name_text, diagnostics)?; - self.linter = Some(linter); - } - "javascript" => { - let mut javascript = JavascriptConfiguration::default(); - javascript.map_to_object(&value, name_text, diagnostics)?; - self.javascript = Some(javascript); - } - "json" => { - let mut json = JsonConfiguration::default(); - json.map_to_object(&value, name_text, diagnostics)?; - self.json = Some(json); - } - "organizeImports" => { - let mut organize_imports = OrganizeImports::default(); - organize_imports.map_to_object(&value, name_text, diagnostics)?; - self.organize_imports = Some(organize_imports); - } - "extends" => { - self.extends = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - "overrides" => { - let mut overrides = Overrides::default(); - overrides.map_to_array(&value, name_text, diagnostics)?; - self.overrides = Some(overrides); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &[ + "vcs", + "files", + "linter", + "formatter", + "javascript", + "json", + "$schema", + "organizeImports", + "extends", + "overrides", + ]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "$schema" => { + result.schema = Deserializable::deserialize(value, diagnostics); + } + "files" => { + result.files = Deserializable::deserialize(value, diagnostics); + } + "vcs" => { + result.vcs = Deserializable::deserialize(value, diagnostics); + } + "formatter" => { + result.formatter = Deserializable::deserialize(value, diagnostics); + } + "linter" => { + result.linter = Deserializable::deserialize(value, diagnostics); + } + "javascript" => { + result.javascript = Deserializable::deserialize(value, diagnostics); + } + "json" => { + result.json = Deserializable::deserialize(value, diagnostics); + } + "organizeImports" => { + result.organize_imports = Deserializable::deserialize(value, diagnostics); + } + "extends" => { + result.extends = Deserializable::deserialize(value, diagnostics); + } + "overrides" => { + result.overrides = Deserializable::deserialize(value, diagnostics); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } } - - Some(()) + Some(result) } } diff --git a/crates/biome_service/src/configuration/parse/json/files.rs b/crates/biome_service/src/configuration/parse/json/files.rs index 90c9df3f9ee1..e25056344acb 100644 --- a/crates/biome_service/src/configuration/parse/json/files.rs +++ b/crates/biome_service/src/configuration/parse/json/files.rs @@ -1,48 +1,58 @@ use crate::configuration::FilesConfiguration; -use biome_deserialize::json::{report_unknown_map_key, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, StringSet, VisitNode}; -use biome_json_syntax::JsonLanguage; -use biome_rowan::SyntaxNode; -use std::num::NonZeroU64; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; +use biome_rowan::{TextRange, TokenText}; -impl FilesConfiguration { - const ALLOWED_KEYS: &'static [&'static str] = - &["maxSize", "ignore", "include", "ignoreUnknown"]; +impl Deserializable for FilesConfiguration { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(FilesConfigurationVisitor, diagnostics) + } } -impl VisitNode for FilesConfiguration { +struct FilesConfigurationVisitor; +impl DeserializationVisitor for FilesConfigurationVisitor { + type Output = FilesConfiguration; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "maxSize" => { - self.max_size = - NonZeroU64::new(self.map_to_u64(&value, name_text, u64::MAX, diagnostics)?); - } - "ignore" => { - self.ignore = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - - "include" => { - self.include = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - "ignoreUnknown" => { - self.ignore_unknown = self.map_to_boolean(&value, name_text, diagnostics); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["maxSize", "ignore", "include", "ignoreUnknown"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "maxSize" => { + result.max_size = Deserializable::deserialize(value, diagnostics); + } + "ignore" => { + result.ignore = Deserializable::deserialize(value, diagnostics); + } + "include" => { + result.include = Deserializable::deserialize(value, diagnostics); + } + "ignoreUnknown" => { + result.ignore_unknown = Deserializable::deserialize(value, diagnostics); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } } - Some(()) + Some(result) } } diff --git a/crates/biome_service/src/configuration/parse/json/formatter.rs b/crates/biome_service/src/configuration/parse/json/formatter.rs index fb2feddd0c42..6b4dc6d80881 100644 --- a/crates/biome_service/src/configuration/parse/json/formatter.rs +++ b/crates/biome_service/src/configuration/parse/json/formatter.rs @@ -1,107 +1,105 @@ use crate::configuration::{FormatterConfiguration, PlainIndentStyle}; -use biome_console::markup; -use biome_deserialize::json::{report_unknown_map_key, report_unknown_variant, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, StringSet, VisitNode}; -use biome_formatter::LineWidth; -use biome_json_syntax::{JsonLanguage, JsonStringValue}; -use biome_rowan::{AstNode, SyntaxNode}; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; +use biome_rowan::{TextRange, TokenText}; -impl FormatterConfiguration { - const ALLOWED_KEYS: &'static [&'static str] = &[ - "enabled", - "formatWithErrors", - "indentStyle", - "indentSize", - "indentWidth", - "lineWidth", - "ignore", - "include", - ]; +impl Deserializable for FormatterConfiguration { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(FormatterConfigurationVisitor, diagnostics) + } } -impl VisitNode for FormatterConfiguration { +struct FormatterConfigurationVisitor; +impl DeserializationVisitor for FormatterConfigurationVisitor { + type Output = FormatterConfiguration; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "enabled" => { - self.enabled = self.map_to_boolean(&value, name_text, diagnostics); - } - "ignore" => { - self.ignore = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - "include" => { - self.include = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - "indentStyle" => { - let mut indent_style = PlainIndentStyle::default(); - indent_style.map_to_known_string(&value, name_text, diagnostics)?; - self.indent_style = Some(indent_style); - } - "indentSize" => { - self.indent_width = self.map_to_u8(&value, name_text, u8::MAX, diagnostics); - diagnostics.push(DeserializationDiagnostic::new_deprecated( - name_text, - key.text_trimmed_range(), - "formatter.indentWidth", - )); - } - "indentWidth" => { - self.indent_width = self.map_to_u8(&value, name_text, u8::MAX, diagnostics); - } - "lineWidth" => { - let line_width = self.map_to_u16(&value, name_text, LineWidth::MAX, diagnostics)?; - self.line_width = Some(match LineWidth::try_from(line_width) { - Ok(result) => result, - Err(err) => { - diagnostics.push( - DeserializationDiagnostic::new(err.to_string()) - .with_range(value.range()) - .with_note( - markup! {"Maximum value accepted is "{{LineWidth::MAX}}}, - ), - ); - LineWidth::default() - } - }); - } - "formatWithErrors" => { - self.format_with_errors = self.map_to_boolean(&value, name_text, diagnostics); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &[ + "enabled", + "formatWithErrors", + "indentStyle", + "indentSize", + "indentWidth", + "lineWidth", + "ignore", + "include", + ]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "enabled" => { + result.enabled = Deserializable::deserialize(value, diagnostics); + } + "ignore" => { + result.ignore = Deserializable::deserialize(value, diagnostics); + } + "include" => { + result.include = Deserializable::deserialize(value, diagnostics); + } + "indentStyle" => { + result.indent_style = Deserializable::deserialize(value, diagnostics); + } + "indentSize" => { + result.indent_width = Deserializable::deserialize(value, diagnostics); + diagnostics.push(DeserializationDiagnostic::new_deprecated( + key.text(), + key_range, + "formatter.indentWidth", + )); + } + "indentWidth" => { + result.indent_width = Deserializable::deserialize(value, diagnostics); + } + "lineWidth" => { + result.line_width = Deserializable::deserialize(value, diagnostics); + } + "formatWithErrors" => { + result.format_with_errors = Deserializable::deserialize(value, diagnostics); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } } - Some(()) + Some(result) } } -impl PlainIndentStyle { - const ALLOWED_VARIANTS: &'static [&'static str] = &["tab", "space"]; -} - -impl VisitNode for PlainIndentStyle { - fn visit_value( - &mut self, - node: &SyntaxNode, +impl Deserializable for PlainIndentStyle { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let node = JsonStringValue::cast_ref(node)?; - if let Ok(value) = node.inner_string_text().ok()?.text().parse::() { - *self = value; + ) -> Option { + const ALLOWED_VARIANTS: &[&str] = &["tab", "space"]; + let range = value.range(); + let value = TokenText::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + Some(value) } else { - report_unknown_variant(&node, Self::ALLOWED_VARIANTS, diagnostics); + diagnostics.push(DeserializationDiagnostic::new_unknown_value( + value.text(), + range, + ALLOWED_VARIANTS, + )); + None } - Some(()) } } diff --git a/crates/biome_service/src/configuration/parse/json/javascript/formatter.rs b/crates/biome_service/src/configuration/parse/json/javascript/formatter.rs index 77e006c526e1..2f04a64c79b5 100644 --- a/crates/biome_service/src/configuration/parse/json/javascript/formatter.rs +++ b/crates/biome_service/src/configuration/parse/json/javascript/formatter.rs @@ -1,114 +1,98 @@ use crate::configuration::javascript::JavascriptFormatter; -use crate::configuration::PlainIndentStyle; -use biome_deserialize::json::{report_unknown_map_key, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, VisitNode}; -use biome_formatter::LineWidth; -use biome_js_formatter::context::trailing_comma::TrailingComma; -use biome_js_formatter::context::{ArrowParentheses, QuoteProperties, QuoteStyle, Semicolons}; -use biome_json_syntax::JsonLanguage; -use biome_rowan::{AstNode, SyntaxNode}; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; +use biome_rowan::TokenText; -impl JavascriptFormatter { - const ALLOWED_KEYS: &'static [&'static str] = &[ - "quoteStyle", - "jsxQuoteStyle", - "quoteProperties", - "trailingComma", - "semicolons", - "arrowParentheses", - "enabled", - "indentStyle", - "indentSize", - "indentWidth", - "lineWidth", - ]; -} - -impl VisitNode for JavascriptFormatter { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, +impl Deserializable for JavascriptFormatter { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "jsxQuoteStyle" => { - let mut jsx_quote_style = QuoteStyle::default(); - jsx_quote_style.map_to_known_string(&value, name_text, diagnostics)?; - self.jsx_quote_style = Some(jsx_quote_style); - } - "quoteStyle" => { - let mut quote_style = QuoteStyle::default(); - quote_style.map_to_known_string(&value, name_text, diagnostics)?; - self.quote_style = Some(quote_style); - } - "trailingComma" => { - let mut trailing_comma = TrailingComma::default(); - trailing_comma.map_to_known_string(&value, name_text, diagnostics)?; - self.trailing_comma = Some(trailing_comma); - } - "quoteProperties" => { - let mut quote_properties = QuoteProperties::default(); - quote_properties.map_to_known_string(&value, name_text, diagnostics)?; - self.quote_properties = Some(quote_properties); - } - "semicolons" => { - let mut semicolons = Semicolons::default(); - semicolons.map_to_known_string(&value, name_text, diagnostics)?; - self.semicolons = Some(semicolons); - } - "arrowParentheses" => { - let mut arrow_parentheses = ArrowParentheses::default(); - arrow_parentheses.map_to_known_string(&value, name_text, diagnostics)?; - self.arrow_parentheses = Some(arrow_parentheses); - } - - "enabled" => { - self.enabled = self.map_to_boolean(&value, name_text, diagnostics); - } + ) -> Option { + value.deserialize(JavascriptFormatterVisitor, diagnostics) + } +} - "indentStyle" => { - let mut indent_style = PlainIndentStyle::default(); - indent_style.map_to_known_string(&value, name_text, diagnostics)?; - self.indent_style = Some(indent_style); - } - "indentSize" => { - self.indent_width = self.map_to_u8(&value, name_text, u8::MAX, diagnostics); - diagnostics.push(DeserializationDiagnostic::new_deprecated( - name_text, - key.text_trimmed_range(), - "javascript.formatter.indentWidth", - )); - } +struct JavascriptFormatterVisitor; +impl DeserializationVisitor for JavascriptFormatterVisitor { + type Output = JavascriptFormatter; - "indentWidth" => { - self.indent_width = self.map_to_u8(&value, name_text, u8::MAX, diagnostics); - } - "lineWidth" => { - let line_width = self.map_to_u16(&value, name_text, LineWidth::MAX, diagnostics)?; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; - self.line_width = Some(match LineWidth::try_from(line_width) { - Ok(result) => result, - Err(err) => { - diagnostics.push( - biome_deserialize::DeserializationDiagnostic::new(err.to_string()) - .with_range(value.range()) - .with_note( - biome_console::markup! {"Maximum value accepted is "{{LineWidth::MAX}}}, - ), - ); - LineWidth::default() - } - }); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + fn visit_map( + self, + members: impl Iterator, + _range: biome_rowan::TextRange, + diagnostics: &mut Vec, + ) -> Option { + const ALLOWED_KEYS: &[&str] = &[ + "quoteStyle", + "jsxQuoteStyle", + "quoteProperties", + "trailingComma", + "semicolons", + "arrowParentheses", + "enabled", + "indentStyle", + "indentSize", + "indentWidth", + "lineWidth", + ]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "jsxQuoteStyle" => { + result.jsx_quote_style = Deserializable::deserialize(value, diagnostics); + } + "quoteStyle" => { + result.quote_style = Deserializable::deserialize(value, diagnostics); + } + "trailingComma" => { + result.trailing_comma = Deserializable::deserialize(value, diagnostics); + } + "quoteProperties" => { + result.quote_properties = Deserializable::deserialize(value, diagnostics); + } + "semicolons" => { + result.semicolons = Deserializable::deserialize(value, diagnostics); + } + "arrowParentheses" => { + result.arrow_parentheses = Deserializable::deserialize(value, diagnostics); + } + "enabled" => { + result.enabled = Deserializable::deserialize(value, diagnostics); + } + "indentStyle" => { + result.indent_style = Deserializable::deserialize(value, diagnostics); + } + "indentSize" => { + result.indent_width = Deserializable::deserialize(value, diagnostics); + diagnostics.push(DeserializationDiagnostic::new_deprecated( + key.text(), + key_range, + "javascript.formatter.indentWidth", + )); + } + "indentWidth" => { + result.indent_width = Deserializable::deserialize(value, diagnostics); + } + "lineWidth" => { + result.line_width = Deserializable::deserialize(value, diagnostics); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )); + } } } - - Some(()) + Some(result) } } diff --git a/crates/biome_service/src/configuration/parse/json/javascript/mod.rs b/crates/biome_service/src/configuration/parse/json/javascript/mod.rs index 14930cde3626..837ec8e0bc0b 100644 --- a/crates/biome_service/src/configuration/parse/json/javascript/mod.rs +++ b/crates/biome_service/src/configuration/parse/json/javascript/mod.rs @@ -1,89 +1,134 @@ mod formatter; use crate::configuration::javascript::{JavascriptOrganizeImports, JavascriptParser}; -use crate::configuration::{JavascriptConfiguration, JavascriptFormatter}; -use biome_deserialize::json::{report_unknown_map_key, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, StringSet, VisitNode}; -use biome_json_syntax::{JsonLanguage, JsonSyntaxNode}; -use biome_rowan::SyntaxNode; +use crate::configuration::JavascriptConfiguration; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; +use biome_rowan::TokenText; -impl JavascriptConfiguration { - const ALLOWED_KEYS: &'static [&'static str] = - &["formatter", "globals", "organizeImports", "parser"]; +impl Deserializable for JavascriptConfiguration { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(JavascriptConfigurationVisitor, diagnostics) + } } -impl VisitNode for JavascriptConfiguration { +struct JavascriptConfigurationVisitor; +impl DeserializationVisitor for JavascriptConfigurationVisitor { + type Output = JavascriptConfiguration; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "formatter" => { - let mut javascript_formatter = JavascriptFormatter::default(); - javascript_formatter.map_to_object(&value, name_text, diagnostics)?; - self.formatter = Some(javascript_formatter); - } - "parser" => { - let mut parser = JavascriptParser::default(); - parser.map_to_object(&value, name_text, diagnostics)?; - self.parser = Some(parser); - } - "globals" => { - self.globals = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - "organizeImports" => { - let mut javascript_organize_imports = JavascriptOrganizeImports::default(); - javascript_organize_imports.map_to_object(&value, name_text, diagnostics)?; - self.organize_imports = Some(javascript_organize_imports); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["formatter", "globals", "organizeImports", "parser"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "formatter" => { + result.formatter = Deserializable::deserialize(value, diagnostics); + } + "parser" => { + result.parser = Deserializable::deserialize(value, diagnostics); + } + "globals" => { + result.globals = Deserializable::deserialize(value, diagnostics); + } + "organizeImports" => { + result.organize_imports = Deserializable::deserialize(value, diagnostics); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )); + } } } + Some(result) + } +} - Some(()) +impl Deserializable for JavascriptOrganizeImports { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(JavascriptOrganizeImportsVisitor, diagnostics) } } -impl VisitNode for JavascriptOrganizeImports { +struct JavascriptOrganizeImportsVisitor; +impl DeserializationVisitor for JavascriptOrganizeImportsVisitor { + type Output = JavascriptOrganizeImports; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - _key: &JsonSyntaxNode, - _value: &JsonSyntaxNode, + self, + _members: impl Iterator, + _range: biome_rowan::TextRange, _diagnostics: &mut Vec, - ) -> Option<()> { - Some(()) + ) -> Option { + Some(Self::Output::default()) } } -impl JavascriptParser { - const ALLOWED_KEYS: &'static [&'static str] = &["unsafeParameterDecoratorsEnabled"]; +impl Deserializable for JavascriptParser { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(JavascriptParserVisitor, diagnostics) + } } -impl VisitNode for JavascriptParser { +struct JavascriptParserVisitor; +impl DeserializationVisitor for JavascriptParserVisitor { + type Output = JavascriptParser; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - if name_text == "unsafeParameterDecoratorsEnabled" { - self.unsafe_parameter_decorators_enabled = - self.map_to_boolean(&value, name_text, diagnostics); - } else { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["unsafeParameterDecoratorsEnabled"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "unsafeParameterDecoratorsEnabled" => { + result.unsafe_parameter_decorators_enabled = + Deserializable::deserialize(value, diagnostics); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )); + } + } } - - Some(()) + Some(result) } } diff --git a/crates/biome_service/src/configuration/parse/json/json_impl/mod.rs b/crates/biome_service/src/configuration/parse/json/json_impl/mod.rs index b50af7f18353..d4ce34df82d6 100644 --- a/crates/biome_service/src/configuration/parse/json/json_impl/mod.rs +++ b/crates/biome_service/src/configuration/parse/json/json_impl/mod.rs @@ -1,121 +1,170 @@ use crate::configuration::json::{JsonConfiguration, JsonFormatter, JsonParser}; -use crate::configuration::PlainIndentStyle; -use biome_deserialize::json::{report_unknown_map_key, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, VisitNode}; -use biome_formatter::LineWidth; -use biome_json_syntax::JsonLanguage; -use biome_rowan::SyntaxNode; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; +use biome_rowan::TokenText; -impl JsonConfiguration { - const ALLOWED_KEYS: &'static [&'static str] = &["parser", "formatter"]; +impl Deserializable for JsonConfiguration { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(JsonConfigurationVisitor, diagnostics) + } } -impl VisitNode for JsonConfiguration { +struct JsonConfigurationVisitor; +impl DeserializationVisitor for JsonConfigurationVisitor { + type Output = JsonConfiguration; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "parser" => { - let mut parser = JsonParser::default(); - parser.map_to_object(&value, name_text, diagnostics)?; - self.parser = Some(parser); - } - "formatter" => { - let mut formatter = JsonFormatter::default(); - formatter.map_to_object(&value, name_text, diagnostics)?; - self.formatter = Some(formatter); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["parser", "formatter"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "parser" => { + result.parser = Deserializable::deserialize(value, diagnostics); + } + "formatter" => { + result.formatter = Deserializable::deserialize(value, diagnostics); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )); + } } } - Some(()) + Some(result) } } -impl JsonParser { - const ALLOWED_KEYS: &'static [&'static str] = &["allowComments", "allowTrailingCommas"]; +impl Deserializable for JsonParser { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(JsonParserVisitor, diagnostics) + } } -impl VisitNode for JsonParser { +struct JsonParserVisitor; +impl DeserializationVisitor for JsonParserVisitor { + type Output = JsonParser; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "allowComments" => { - self.allow_comments = self.map_to_boolean(&value, name_text, diagnostics); - } - "allowTrailingCommas" => { - self.allow_trailing_commas = self.map_to_boolean(&value, name_text, diagnostics); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["allowComments", "allowTrailingCommas"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "allowComments" => { + result.allow_comments = Deserializable::deserialize(value, diagnostics); + } + "allowTrailingCommas" => { + result.allow_trailing_commas = Deserializable::deserialize(value, diagnostics); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )); + } } } - Some(()) + Some(result) } } -impl JsonFormatter { - const ALLOWED_KEYS: &'static [&'static str] = &[ - "enabled", - "indentStyle", - "indentSize", - "indentWidth", - "lineWidth", - ]; +impl Deserializable for JsonFormatter { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(JsonFormatterVisitor, diagnostics) + } } -impl VisitNode for JsonFormatter { +struct JsonFormatterVisitor; +impl DeserializationVisitor for JsonFormatterVisitor { + type Output = JsonFormatter; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "enabled" => { - self.enabled = self.map_to_boolean(&value, name_text, diagnostics); - } - "indentStyle" => { - let mut indent_style = PlainIndentStyle::default(); - indent_style.map_to_known_string(&value, name_text, diagnostics)?; - self.indent_style = Some(indent_style); - } - "indentSize" => { - self.indent_width = self.map_to_u8(&value, name_text, u8::MAX, diagnostics); - diagnostics.push(DeserializationDiagnostic::new_deprecated( - name_text, - key.text_trimmed_range(), - "json.formatter.indentWidth", - )); - } - "indentWidth" => { - self.indent_width = self.map_to_u8(&value, name_text, u8::MAX, diagnostics); - } - "lineWidth" => { - let line_width = self.map_to_u16(&value, name_text, LineWidth::MAX, diagnostics)?; - self.line_width = LineWidth::try_from(line_width).ok(); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &[ + "enabled", + "indentStyle", + "indentSize", + "indentWidth", + "lineWidth", + ]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "enabled" => { + result.enabled = Deserializable::deserialize(value, diagnostics); + } + "indentStyle" => { + result.indent_style = Deserializable::deserialize(value, diagnostics); + } + "indentSize" => { + result.indent_width = Deserializable::deserialize(value, diagnostics); + diagnostics.push(DeserializationDiagnostic::new_deprecated( + key.text(), + key_range, + "json.formatter.indentWidth", + )); + } + "indentWidth" => { + result.indent_width = Deserializable::deserialize(value, diagnostics); + } + "lineWidth" => { + result.line_width = Deserializable::deserialize(value, diagnostics); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )); + } } } - Some(()) + Some(result) } } diff --git a/crates/biome_service/src/configuration/parse/json/linter.rs b/crates/biome_service/src/configuration/parse/json/linter.rs index eacd4872f921..06617d4aa59b 100644 --- a/crates/biome_service/src/configuration/parse/json/linter.rs +++ b/crates/biome_service/src/configuration/parse/json/linter.rs @@ -1,208 +1,156 @@ use crate::configuration::linter::{RulePlainConfiguration, RuleWithOptions}; use crate::configuration::LinterConfiguration; -use crate::{RuleConfiguration, Rules}; -use biome_console::markup; -use biome_deserialize::json::{report_unknown_map_key, report_unknown_variant, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, StringSet, VisitNode}; +use crate::RuleConfiguration; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; use biome_js_analyze::options::PossibleOptions; -use biome_json_syntax::{AnyJsonValue, JsonLanguage, JsonObjectValue, JsonStringValue}; -use biome_rowan::{AstNode, AstSeparatedList, SyntaxNode}; +use biome_rowan::{TextRange, TokenText}; -impl LinterConfiguration { - const ALLOWED_KEYS: &'static [&'static str] = &["enabled", "rules", "include", "ignore"]; +impl Deserializable for LinterConfiguration { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(LinterConfigurationVisitor, diagnostics) + } } -impl VisitNode for LinterConfiguration { +struct LinterConfigurationVisitor; +impl DeserializationVisitor for LinterConfigurationVisitor { + type Output = LinterConfiguration; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "ignore" => { - self.ignore = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - "include" => { - self.include = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - "enabled" => { - self.enabled = self.map_to_boolean(&value, name_text, diagnostics); - } - "rules" => { - if are_recommended_and_all_correct(&value, name_text, diagnostics)? { - let mut rules = Rules::default(); - rules.map_to_object(&value, name_text, diagnostics)?; - self.rules = Some(rules); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["enabled", "rules", "include", "ignore"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "ignore" => { + result.ignore = Deserializable::deserialize(value, diagnostics); } - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + "include" => { + result.include = Deserializable::deserialize(value, diagnostics); + } + "enabled" => { + result.enabled = Deserializable::deserialize(value, diagnostics); + } + "rules" => { + result.rules = Deserializable::deserialize(value, diagnostics); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } } - Some(()) + Some(result) } } impl RuleConfiguration { - pub(crate) fn map_rule_configuration( - &mut self, - value: &AnyJsonValue, + pub(crate) fn deserialize_from_rule_name( rule_name: &str, + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - match value { - AnyJsonValue::JsonStringValue(_) => { - self.map_to_known_string(value, rule_name, diagnostics)?; - } - AnyJsonValue::JsonObjectValue(_) => { - let with_options = RuleWithOptions { - level: RulePlainConfiguration::default(), - options: PossibleOptions::new_from_rule_name(rule_name), - }; - let mut configuration = RuleConfiguration::WithOptions(with_options); - configuration.map_to_object(value, rule_name, diagnostics)?; - *self = configuration; - } - _ => { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type( - "object or string", - value.range(), - )); - } - } - Some(()) + ) -> Option { + value.deserialize(RuleConfigurationVisitor { rule_name }, diagnostics) } } -impl VisitNode for RuleConfiguration { - fn visit_value( - &mut self, - node: &SyntaxNode, - diagnostics: &mut Vec, - ) -> Option<()> { - if let Self::Plain(plain) = self { - plain.visit_value(node, diagnostics)?; - } - Some(()) - } - - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, - diagnostics: &mut Vec, - ) -> Option<()> { - if let Self::WithOptions(with_options) = self { - with_options.visit_map(key, value, diagnostics)?; - } - Some(()) - } +struct RuleConfigurationVisitor<'a> { + rule_name: &'a str, } -impl RulePlainConfiguration { - const ALLOWED_VARIANTS: &'static [&'static str] = &["error", "warn", "off"]; -} +impl<'a> DeserializationVisitor for RuleConfigurationVisitor<'a> { + type Output = RuleConfiguration; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::STR.union(ExpectedType::MAP); -impl VisitNode for RulePlainConfiguration { - fn visit_value( - &mut self, - node: &SyntaxNode, + fn visit_str( + self, + value: TokenText, + range: TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let node = JsonStringValue::cast_ref(node)?; - if let Ok(value) = node.inner_string_text().ok()?.text().parse::() { - *self = value; - } else { - report_unknown_variant(&node, Self::ALLOWED_VARIANTS, diagnostics); - } - Some(()) + ) -> Option { + RulePlainConfiguration::deserialize_from_str(value, range, diagnostics) + .map(RuleConfiguration::Plain) } -} -impl RuleWithOptions { - const ALLOWED_KEYS: &'static [&'static str] = &["level", "options"]; -} - -impl VisitNode for RuleWithOptions { fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "level" => { - self.level.visit_value(value.syntax(), diagnostics)?; - } - "options" => { - let mut possible_options = self.options.take()?; - possible_options.map_to_object(&value, name_text, diagnostics); - self.options = Some(possible_options); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["level", "options"]; + let mut result = RuleWithOptions::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "level" => { + result.level = Deserializable::deserialize(value, diagnostics)?; + } + "options" => { + result.options = PossibleOptions::deserialize_from_rule_name( + self.rule_name, + value, + diagnostics, + ); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } } - Some(()) + Some(RuleConfiguration::WithOptions(result)) } } -/// Custom validation. It checks that `recommended` and `all` are not present and both `true` in the same object -pub(crate) fn are_recommended_and_all_correct( - current_node: &AnyJsonValue, - name: &str, - diagnostics: &mut Vec, -) -> Option { - let value = JsonObjectValue::cast_ref(current_node.syntax()).or_else(|| { - diagnostics.push(DeserializationDiagnostic::new_incorrect_type_for_value( - name, - "object", - current_node.range(), - )); - None - })?; - - let recommended = value.json_member_list().iter().find_map(|member| { - let member = member.ok()?; - if member.name().ok()?.inner_string_text().ok()?.text() == "recommended" { - member.value().ok()?.as_json_boolean_value().cloned() - } else { - None - } - }); - - let all = value.json_member_list().iter().find_map(|member| { - let member = member.ok()?; - if member.name().ok()?.inner_string_text().ok()?.text() == "all" { - member.value().ok()?.as_json_boolean_value().cloned() +impl RulePlainConfiguration { + fn deserialize_from_str( + value: TokenText, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + const ALLOWED_VARIANTS: &[&str] = &["error", "warn", "off"]; + if let Ok(value) = value.text().parse::() { + Some(value) } else { + diagnostics.push(DeserializationDiagnostic::new_unknown_value( + value.text(), + range, + ALLOWED_VARIANTS, + )); None } - }); + } +} - if let (Some(recommended), Some(all)) = (recommended, all) { - if recommended.value_token().ok()?.text() == "true" - && all.value_token().ok()?.text() == "true" - { - diagnostics - .push(DeserializationDiagnostic::new(markup!( - "'recommended'"" and ""'all'"" can't be both ""'true'"". You should choose only one of them." - )) - .with_range(current_node.range()) - .with_note(markup!("Biome will fallback to its defaults for this section."))); - return Some(false); - } +impl Deserializable for RulePlainConfiguration { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + let range = value.range(); + let value = TokenText::deserialize(value, diagnostics)?; + Self::deserialize_from_str(value, range, diagnostics) } - Some(true) } diff --git a/crates/biome_service/src/configuration/parse/json/mod.rs b/crates/biome_service/src/configuration/parse/json/mod.rs index 3798f5606c05..8197133d400b 100644 --- a/crates/biome_service/src/configuration/parse/json/mod.rs +++ b/crates/biome_service/src/configuration/parse/json/mod.rs @@ -11,37 +11,3 @@ mod organize_imports; mod overrides; mod rules; mod vcs; - -use crate::Configuration; -use biome_deserialize::json::{JsonDeserialize, VisitJsonNode}; -use biome_deserialize::DeserializationDiagnostic; -use biome_json_syntax::{AnyJsonValue, JsonRoot}; -use biome_rowan::AstNode; - -impl JsonDeserialize for Configuration { - fn deserialize_from_ast( - root: &JsonRoot, - visitor: &mut impl VisitJsonNode, - diagnostics: &mut Vec, - ) -> Option<()> { - let value = root.value().ok()?; - match value { - AnyJsonValue::JsonObjectValue(node) => { - for element in node.json_member_list() { - let element = element.ok()?; - let member_name = element.name().ok()?; - let member_value = element.value().ok()?; - visitor.visit_map(member_name.syntax(), member_value.syntax(), diagnostics)?; - } - Some(()) - } - _ => { - diagnostics.push( - DeserializationDiagnostic::new("The configuration should be an object") - .with_range(root.range()), - ); - None - } - } - } -} diff --git a/crates/biome_service/src/configuration/parse/json/organize_imports.rs b/crates/biome_service/src/configuration/parse/json/organize_imports.rs index 6d30d9f0bcc8..4a9ac10404f2 100644 --- a/crates/biome_service/src/configuration/parse/json/organize_imports.rs +++ b/crates/biome_service/src/configuration/parse/json/organize_imports.rs @@ -1,42 +1,55 @@ use crate::configuration::organize_imports::OrganizeImports; -use biome_deserialize::json::{report_unknown_map_key, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, StringSet, VisitNode}; -use biome_json_syntax::JsonLanguage; -use biome_rowan::SyntaxNode; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; +use biome_rowan::{TextRange, TokenText}; -impl OrganizeImports { - const ALLOWED_KEYS: &'static [&'static str] = &["enabled", "ignore", "include"]; +impl Deserializable for OrganizeImports { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(OrganizeImportsVisitor, diagnostics) + } } -impl VisitNode for OrganizeImports { +struct OrganizeImportsVisitor; +impl DeserializationVisitor for OrganizeImportsVisitor { + type Output = OrganizeImports; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + _range: TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "enabled" => { - self.enabled = self.map_to_boolean(&value, name_text, diagnostics); - } - "ignore" => { - self.ignore = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - "include" => { - self.include = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["enabled", "ignore", "include"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "enabled" => { + result.enabled = Deserializable::deserialize(value, diagnostics); + } + "ignore" => { + result.ignore = Deserializable::deserialize(value, diagnostics); + } + "include" => { + result.include = Deserializable::deserialize(value, diagnostics); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } } - - Some(()) + Some(result) } } diff --git a/crates/biome_service/src/configuration/parse/json/overrides.rs b/crates/biome_service/src/configuration/parse/json/overrides.rs index 395314d4ea7d..e8aee2005f1b 100644 --- a/crates/biome_service/src/configuration/parse/json/overrides.rs +++ b/crates/biome_service/src/configuration/parse/json/overrides.rs @@ -2,222 +2,245 @@ use crate::configuration::overrides::{ OverrideFormatterConfiguration, OverrideLinterConfiguration, OverrideOrganizeImportsConfiguration, OverridePattern, Overrides, }; -use crate::configuration::parse::json::linter::are_recommended_and_all_correct; -use crate::configuration::{JavascriptConfiguration, JsonConfiguration, PlainIndentStyle}; -use crate::Rules; -use biome_console::markup; -use biome_deserialize::json::{report_unknown_map_key, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, StringSet, VisitNode}; -use biome_formatter::LineWidth; -use biome_json_syntax::{AnyJsonValue, JsonLanguage, JsonSyntaxNode}; -use biome_rowan::AstNode; - -impl VisitNode for Overrides { - fn visit_array_member( - &mut self, - element: &JsonSyntaxNode, +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; +use biome_rowan::TokenText; + +impl Deserializable for Overrides { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let mut pattern = OverridePattern::default(); - let element = AnyJsonValue::cast_ref(element)?; - pattern.map_to_object(&element, "overrides", diagnostics)?; - self.0.push(pattern); - Some(()) + ) -> Option { + Some(Overrides(Deserializable::deserialize(value, diagnostics)?)) } } -impl OverridePattern { - const ALLOWED_KEYS: &'static [&'static str] = &[ - "ignore", - "include", - "formatter", - "linter", - "organizeImports", - "javascript", - "json", - ]; +impl Deserializable for OverridePattern { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(OverridePatternVisitor, diagnostics) + } } -impl VisitNode for OverridePattern { +struct OverridePatternVisitor; +impl DeserializationVisitor for OverridePatternVisitor { + type Output = OverridePattern; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &JsonSyntaxNode, - value: &JsonSyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "ignore" => { - self.ignore = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - "include" => { - self.include = self - .map_to_index_set_string(&value, name_text, diagnostics) - .map(StringSet::new); - } - "formatter" => { - let mut formatter = OverrideFormatterConfiguration::default(); - formatter.map_to_object(&value, name_text, diagnostics)?; - self.formatter = Some(formatter); - } - "linter" => { - let mut linter = OverrideLinterConfiguration::default(); - linter.map_to_object(&value, name_text, diagnostics)?; - self.linter = Some(linter); - } - "organizeImports" => { - let mut organize_imports = OverrideOrganizeImportsConfiguration::default(); - organize_imports.map_to_object(&value, name_text, diagnostics)?; - self.organize_imports = Some(organize_imports); - } - "javascript" => { - let mut javascript = JavascriptConfiguration::default(); - javascript.map_to_object(&value, name_text, diagnostics)?; - self.javascript = Some(javascript); - } - "json" => { - let mut json = JsonConfiguration::default(); - json.map_to_object(&value, name_text, diagnostics)?; - self.json = Some(json); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &[ + "ignore", + "include", + "formatter", + "linter", + "organizeImports", + "javascript", + "json", + ]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "ignore" => { + result.ignore = Deserializable::deserialize(value, diagnostics); + } + "include" => { + result.include = Deserializable::deserialize(value, diagnostics); + } + "formatter" => { + result.formatter = Deserializable::deserialize(value, diagnostics); + } + "linter" => { + result.linter = Deserializable::deserialize(value, diagnostics); + } + "organizeImports" => { + result.organize_imports = Deserializable::deserialize(value, diagnostics); + } + "javascript" => { + result.javascript = Deserializable::deserialize(value, diagnostics); + } + "json" => { + result.json = Deserializable::deserialize(value, diagnostics); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } } - - Some(()) + Some(result) } } -impl OverrideFormatterConfiguration { - const ALLOWED_KEYS: &'static [&'static str] = &[ - "enabled", - "formatWithErrors", - "indentStyle", - "indentSize", - "indentWidth", - "lineWidth", - ]; +impl Deserializable for OverrideFormatterConfiguration { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(OverrideFormatterConfigurationVisitor, diagnostics) + } } -impl VisitNode for OverrideFormatterConfiguration { +struct OverrideFormatterConfigurationVisitor; +impl DeserializationVisitor for OverrideFormatterConfigurationVisitor { + type Output = OverrideFormatterConfiguration; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &JsonSyntaxNode, - value: &JsonSyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "enabled" => { - self.enabled = self.map_to_boolean(&value, name_text, diagnostics); - } - - "indentStyle" => { - let mut indent_style = PlainIndentStyle::default(); - indent_style.map_to_known_string(&value, name_text, diagnostics)?; - self.indent_style = Some(indent_style); - } - "indentSize" => { - self.indent_width = self.map_to_u8(&value, name_text, u8::MAX, diagnostics); - diagnostics.push(DeserializationDiagnostic::new_deprecated( - name_text, - key.text_trimmed_range(), - "formatter.indentWidth", - )); - } - "indentWidth" => { - self.indent_width = self.map_to_u8(&value, name_text, u8::MAX, diagnostics); - } - "lineWidth" => { - let line_width = self.map_to_u16(&value, name_text, LineWidth::MAX, diagnostics)?; - - self.line_width = Some(match LineWidth::try_from(line_width) { - Ok(result) => result, - Err(err) => { - diagnostics.push( - DeserializationDiagnostic::new(err.to_string()) - .with_range(value.range()) - .with_note( - markup! {"Maximum value accepted is "{{LineWidth::MAX}}}, - ), - ); - LineWidth::default() - } - }); - } - "formatWithErrors" => { - self.format_with_errors = self.map_to_boolean(&value, name_text, diagnostics); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &[ + "enabled", + "formatWithErrors", + "indentStyle", + "indentSize", + "indentWidth", + "lineWidth", + ]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "enabled" => { + result.enabled = Deserializable::deserialize(value, diagnostics); + } + "indentStyle" => { + result.indent_style = Deserializable::deserialize(value, diagnostics); + } + "indentSize" => { + result.indent_width = Deserializable::deserialize(value, diagnostics); + diagnostics.push(DeserializationDiagnostic::new_deprecated( + key.text(), + key_range, + "formatter.indentWidth", + )); + } + "indentWidth" => { + result.indent_width = Deserializable::deserialize(value, diagnostics); + } + "lineWidth" => { + result.line_width = Deserializable::deserialize(value, diagnostics); + } + "formatWithErrors" => { + result.format_with_errors = Deserializable::deserialize(value, diagnostics); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } } - - Some(()) + Some(result) } } -impl OverrideLinterConfiguration { - const ALLOWED_KEYS: &'static [&'static str] = &["enabled", "rules"]; +impl Deserializable for OverrideLinterConfiguration { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(OverrideLinterConfigurationVisitor, diagnostics) + } } -impl VisitNode for OverrideLinterConfiguration { +struct OverrideLinterConfigurationVisitor; +impl DeserializationVisitor for OverrideLinterConfigurationVisitor { + type Output = OverrideLinterConfiguration; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &JsonSyntaxNode, - value: &JsonSyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "enabled" => { - self.enabled = self.map_to_boolean(&value, name_text, diagnostics); - } - "rules" => { - let mut rules = Rules::default(); - if are_recommended_and_all_correct(&value, name_text, diagnostics)? { - rules.map_to_object(&value, name_text, diagnostics)?; - self.rules = Some(rules); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["enabled", "rules"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "enabled" => { + result.enabled = Deserializable::deserialize(value, diagnostics); } - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + "rules" => { + result.rules = Deserializable::deserialize(value, diagnostics); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), } } - - Some(()) + Some(result) } } -impl OverrideOrganizeImportsConfiguration { - const ALLOWED_KEYS: &'static [&'static str] = &["enabled"]; +impl Deserializable for OverrideOrganizeImportsConfiguration { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(OverrideOrganizeImportsConfigurationVisitor, diagnostics) + } } -impl VisitNode for OverrideOrganizeImportsConfiguration { +struct OverrideOrganizeImportsConfigurationVisitor; +impl DeserializationVisitor for OverrideOrganizeImportsConfigurationVisitor { + type Output = OverrideOrganizeImportsConfiguration; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &JsonSyntaxNode, - value: &JsonSyntaxNode, + self, + members: impl Iterator, + _range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - if name_text == "enabled" { - self.enabled = self.map_to_boolean(&value, name_text, diagnostics); - } else { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["enabled"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "enabled" => { + result.enabled = Deserializable::deserialize(value, diagnostics); + } + _ => diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + ALLOWED_KEYS, + )), + } } - - Some(()) + Some(result) } } diff --git a/crates/biome_service/src/configuration/parse/json/rules.rs b/crates/biome_service/src/configuration/parse/json/rules.rs index e742233cfb19..344bbe0c800c 100644 --- a/crates/biome_service/src/configuration/parse/json/rules.rs +++ b/crates/biome_service/src/configuration/parse/json/rules.rs @@ -1,1597 +1,2017 @@ //! Generated file, do not edit by hand, see `xtask/codegen` use crate::configuration::linter::*; -use crate::configuration::parse::json::linter::are_recommended_and_all_correct; use crate::Rules; -use biome_deserialize::json::{report_unknown_map_key, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, VisitNode}; -use biome_json_syntax::JsonLanguage; -use biome_rowan::SyntaxNode; -impl VisitNode for Rules { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, +use biome_console::markup; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; +use biome_rowan::{TextRange, TokenText}; +impl Deserializable for Rules { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "recommended" => { - self.recommended = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "all" => { - self.all = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "a11y" => { - let mut visitor = A11y::default(); - if are_recommended_and_all_correct(&value, name_text, diagnostics)? { - visitor.map_to_object(&value, name_text, diagnostics)?; - self.a11y = Some(visitor); - } - } - "complexity" => { - let mut visitor = Complexity::default(); - if are_recommended_and_all_correct(&value, name_text, diagnostics)? { - visitor.map_to_object(&value, name_text, diagnostics)?; - self.complexity = Some(visitor); - } - } - "correctness" => { - let mut visitor = Correctness::default(); - if are_recommended_and_all_correct(&value, name_text, diagnostics)? { - visitor.map_to_object(&value, name_text, diagnostics)?; - self.correctness = Some(visitor); - } - } - "nursery" => { - let mut visitor = Nursery::default(); - if are_recommended_and_all_correct(&value, name_text, diagnostics)? { - visitor.map_to_object(&value, name_text, diagnostics)?; - self.nursery = Some(visitor); - } - } - "performance" => { - let mut visitor = Performance::default(); - if are_recommended_and_all_correct(&value, name_text, diagnostics)? { - visitor.map_to_object(&value, name_text, diagnostics)?; - self.performance = Some(visitor); - } - } - "security" => { - let mut visitor = Security::default(); - if are_recommended_and_all_correct(&value, name_text, diagnostics)? { - visitor.map_to_object(&value, name_text, diagnostics)?; - self.security = Some(visitor); - } - } - "style" => { - let mut visitor = Style::default(); - if are_recommended_and_all_correct(&value, name_text, diagnostics)? { - visitor.map_to_object(&value, name_text, diagnostics)?; - self.style = Some(visitor); + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = Rules; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut recommended_is_set = false; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "recommended" => { + recommended_is_set = true; + result.recommended = Deserializable::deserialize(value, diagnostics); + } + "all" => { + result.all = Deserializable::deserialize(value, diagnostics); + } + "a11y" => { + result.a11y = Deserializable::deserialize(value, diagnostics); + } + "complexity" => { + result.complexity = Deserializable::deserialize(value, diagnostics); + } + "correctness" => { + result.correctness = Deserializable::deserialize(value, diagnostics); + } + "nursery" => { + result.nursery = Deserializable::deserialize(value, diagnostics); + } + "performance" => { + result.performance = Deserializable::deserialize(value, diagnostics); + } + "security" => { + result.security = Deserializable::deserialize(value, diagnostics); + } + "style" => { + result.style = Deserializable::deserialize(value, diagnostics); + } + "suspicious" => { + result.suspicious = Deserializable::deserialize(value, diagnostics); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + &[ + "recommended", + "all", + "a11y", + "complexity", + "correctness", + "nursery", + "performance", + "security", + "style", + "suspicious", + ], + )); + } + } } - } - "suspicious" => { - let mut visitor = Suspicious::default(); - if are_recommended_and_all_correct(&value, name_text, diagnostics)? { - visitor.map_to_object(&value, name_text, diagnostics)?; - self.suspicious = Some(visitor); + if recommended_is_set + && matches!(result.recommended, Some(true)) + && matches!(result.all, Some(true)) + { + diagnostics . push (DeserializationDiagnostic :: new (markup ! (< Emphasis > "'recommended'" < / Emphasis > " and " < Emphasis > "'all'" < / Emphasis > " can't be both " < Emphasis > "'true'" < / Emphasis > ". You should choose only one of them.")) . with_range (range) . with_note (markup ! ("Biome will fallback to its defaults for this section."))) ; + return Some(Self::Output::default()); } - } - _ => { - report_unknown_map_key( - &name, - &[ - "recommended", - "all", - "a11y", - "complexity", - "correctness", - "nursery", - "performance", - "security", - "style", - "suspicious", - ], - diagnostics, - ); + Some(result) } } - Some(()) + value.deserialize(Visitor, diagnostics) } } -impl VisitNode for A11y { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, +impl Deserializable for A11y { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "recommended" => { - self.recommended = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "all" => { - self.all = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "noAccessKey" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noAccessKey", diagnostics)?; - self.no_access_key = Some(configuration); - } - "noAriaUnsupportedElements" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noAriaUnsupportedElements", - diagnostics, - )?; - self.no_aria_unsupported_elements = Some(configuration); - } - "noAutofocus" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noAutofocus", diagnostics)?; - self.no_autofocus = Some(configuration); - } - "noBlankTarget" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noBlankTarget", diagnostics)?; - self.no_blank_target = Some(configuration); - } - "noDistractingElements" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noDistractingElements", - diagnostics, - )?; - self.no_distracting_elements = Some(configuration); - } - "noHeaderScope" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noHeaderScope", diagnostics)?; - self.no_header_scope = Some(configuration); - } - "noNoninteractiveElementToInteractiveRole" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noNoninteractiveElementToInteractiveRole", - diagnostics, - )?; - self.no_noninteractive_element_to_interactive_role = Some(configuration); - } - "noNoninteractiveTabindex" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noNoninteractiveTabindex", - diagnostics, - )?; - self.no_noninteractive_tabindex = Some(configuration); - } - "noPositiveTabindex" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noPositiveTabindex", diagnostics)?; - self.no_positive_tabindex = Some(configuration); - } - "noRedundantAlt" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noRedundantAlt", diagnostics)?; - self.no_redundant_alt = Some(configuration); - } - "noRedundantRoles" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noRedundantRoles", diagnostics)?; - self.no_redundant_roles = Some(configuration); - } - "noSvgWithoutTitle" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noSvgWithoutTitle", diagnostics)?; - self.no_svg_without_title = Some(configuration); - } - "useAltText" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useAltText", diagnostics)?; - self.use_alt_text = Some(configuration); - } - "useAnchorContent" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useAnchorContent", diagnostics)?; - self.use_anchor_content = Some(configuration); - } - "useAriaPropsForRole" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useAriaPropsForRole", diagnostics)?; - self.use_aria_props_for_role = Some(configuration); - } - "useButtonType" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useButtonType", diagnostics)?; - self.use_button_type = Some(configuration); - } - "useHeadingContent" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useHeadingContent", diagnostics)?; - self.use_heading_content = Some(configuration); - } - "useHtmlLang" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useHtmlLang", diagnostics)?; - self.use_html_lang = Some(configuration); - } - "useIframeTitle" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useIframeTitle", diagnostics)?; - self.use_iframe_title = Some(configuration); - } - "useKeyWithClickEvents" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useKeyWithClickEvents", - diagnostics, - )?; - self.use_key_with_click_events = Some(configuration); - } - "useKeyWithMouseEvents" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useKeyWithMouseEvents", - diagnostics, - )?; - self.use_key_with_mouse_events = Some(configuration); - } - "useMediaCaption" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useMediaCaption", diagnostics)?; - self.use_media_caption = Some(configuration); - } - "useValidAnchor" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useValidAnchor", diagnostics)?; - self.use_valid_anchor = Some(configuration); - } - "useValidAriaProps" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useValidAriaProps", diagnostics)?; - self.use_valid_aria_props = Some(configuration); - } - "useValidAriaValues" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useValidAriaValues", diagnostics)?; - self.use_valid_aria_values = Some(configuration); - } - "useValidLang" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useValidLang", diagnostics)?; - self.use_valid_lang = Some(configuration); - } - _ => { - report_unknown_map_key( - &name, - &[ - "recommended", - "all", - "noAccessKey", - "noAriaUnsupportedElements", - "noAutofocus", - "noBlankTarget", - "noDistractingElements", - "noHeaderScope", - "noNoninteractiveElementToInteractiveRole", - "noNoninteractiveTabindex", - "noPositiveTabindex", - "noRedundantAlt", - "noRedundantRoles", - "noSvgWithoutTitle", - "useAltText", - "useAnchorContent", - "useAriaPropsForRole", - "useButtonType", - "useHeadingContent", - "useHtmlLang", - "useIframeTitle", - "useKeyWithClickEvents", - "useKeyWithMouseEvents", - "useMediaCaption", - "useValidAnchor", - "useValidAriaProps", - "useValidAriaValues", - "useValidLang", - ], - diagnostics, - ); + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = A11y; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut recommended_is_set = false; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "recommended" => { + recommended_is_set = true; + result.recommended = Deserializable::deserialize(value, diagnostics); + } + "all" => { + result.all = Deserializable::deserialize(value, diagnostics); + } + "noAccessKey" => { + result.no_access_key = RuleConfiguration::deserialize_from_rule_name( + "noAccessKey", + value, + diagnostics, + ); + } + "noAriaUnsupportedElements" => { + result.no_aria_unsupported_elements = + RuleConfiguration::deserialize_from_rule_name( + "noAriaUnsupportedElements", + value, + diagnostics, + ); + } + "noAutofocus" => { + result.no_autofocus = RuleConfiguration::deserialize_from_rule_name( + "noAutofocus", + value, + diagnostics, + ); + } + "noBlankTarget" => { + result.no_blank_target = RuleConfiguration::deserialize_from_rule_name( + "noBlankTarget", + value, + diagnostics, + ); + } + "noDistractingElements" => { + result.no_distracting_elements = + RuleConfiguration::deserialize_from_rule_name( + "noDistractingElements", + value, + diagnostics, + ); + } + "noHeaderScope" => { + result.no_header_scope = RuleConfiguration::deserialize_from_rule_name( + "noHeaderScope", + value, + diagnostics, + ); + } + "noNoninteractiveElementToInteractiveRole" => { + result.no_noninteractive_element_to_interactive_role = + RuleConfiguration::deserialize_from_rule_name( + "noNoninteractiveElementToInteractiveRole", + value, + diagnostics, + ); + } + "noNoninteractiveTabindex" => { + result.no_noninteractive_tabindex = + RuleConfiguration::deserialize_from_rule_name( + "noNoninteractiveTabindex", + value, + diagnostics, + ); + } + "noPositiveTabindex" => { + result.no_positive_tabindex = + RuleConfiguration::deserialize_from_rule_name( + "noPositiveTabindex", + value, + diagnostics, + ); + } + "noRedundantAlt" => { + result.no_redundant_alt = RuleConfiguration::deserialize_from_rule_name( + "noRedundantAlt", + value, + diagnostics, + ); + } + "noRedundantRoles" => { + result.no_redundant_roles = + RuleConfiguration::deserialize_from_rule_name( + "noRedundantRoles", + value, + diagnostics, + ); + } + "noSvgWithoutTitle" => { + result.no_svg_without_title = + RuleConfiguration::deserialize_from_rule_name( + "noSvgWithoutTitle", + value, + diagnostics, + ); + } + "useAltText" => { + result.use_alt_text = RuleConfiguration::deserialize_from_rule_name( + "useAltText", + value, + diagnostics, + ); + } + "useAnchorContent" => { + result.use_anchor_content = + RuleConfiguration::deserialize_from_rule_name( + "useAnchorContent", + value, + diagnostics, + ); + } + "useAriaPropsForRole" => { + result.use_aria_props_for_role = + RuleConfiguration::deserialize_from_rule_name( + "useAriaPropsForRole", + value, + diagnostics, + ); + } + "useButtonType" => { + result.use_button_type = RuleConfiguration::deserialize_from_rule_name( + "useButtonType", + value, + diagnostics, + ); + } + "useHeadingContent" => { + result.use_heading_content = + RuleConfiguration::deserialize_from_rule_name( + "useHeadingContent", + value, + diagnostics, + ); + } + "useHtmlLang" => { + result.use_html_lang = RuleConfiguration::deserialize_from_rule_name( + "useHtmlLang", + value, + diagnostics, + ); + } + "useIframeTitle" => { + result.use_iframe_title = RuleConfiguration::deserialize_from_rule_name( + "useIframeTitle", + value, + diagnostics, + ); + } + "useKeyWithClickEvents" => { + result.use_key_with_click_events = + RuleConfiguration::deserialize_from_rule_name( + "useKeyWithClickEvents", + value, + diagnostics, + ); + } + "useKeyWithMouseEvents" => { + result.use_key_with_mouse_events = + RuleConfiguration::deserialize_from_rule_name( + "useKeyWithMouseEvents", + value, + diagnostics, + ); + } + "useMediaCaption" => { + result.use_media_caption = + RuleConfiguration::deserialize_from_rule_name( + "useMediaCaption", + value, + diagnostics, + ); + } + "useValidAnchor" => { + result.use_valid_anchor = RuleConfiguration::deserialize_from_rule_name( + "useValidAnchor", + value, + diagnostics, + ); + } + "useValidAriaProps" => { + result.use_valid_aria_props = + RuleConfiguration::deserialize_from_rule_name( + "useValidAriaProps", + value, + diagnostics, + ); + } + "useValidAriaValues" => { + result.use_valid_aria_values = + RuleConfiguration::deserialize_from_rule_name( + "useValidAriaValues", + value, + diagnostics, + ); + } + "useValidLang" => { + result.use_valid_lang = RuleConfiguration::deserialize_from_rule_name( + "useValidLang", + value, + diagnostics, + ); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + &[ + "recommended", + "all", + "noAccessKey", + "noAriaUnsupportedElements", + "noAutofocus", + "noBlankTarget", + "noDistractingElements", + "noHeaderScope", + "noNoninteractiveElementToInteractiveRole", + "noNoninteractiveTabindex", + "noPositiveTabindex", + "noRedundantAlt", + "noRedundantRoles", + "noSvgWithoutTitle", + "useAltText", + "useAnchorContent", + "useAriaPropsForRole", + "useButtonType", + "useHeadingContent", + "useHtmlLang", + "useIframeTitle", + "useKeyWithClickEvents", + "useKeyWithMouseEvents", + "useMediaCaption", + "useValidAnchor", + "useValidAriaProps", + "useValidAriaValues", + "useValidLang", + ], + )); + } + } + } + if recommended_is_set + && matches!(result.recommended, Some(true)) + && matches!(result.all, Some(true)) + { + diagnostics . push (DeserializationDiagnostic :: new (markup ! (< Emphasis > "'recommended'" < / Emphasis > " and " < Emphasis > "'all'" < / Emphasis > " can't be both " < Emphasis > "'true'" < / Emphasis > ". You should choose only one of them.")) . with_range (range) . with_note (markup ! ("Biome will fallback to its defaults for this section."))) ; + return Some(Self::Output::default()); + } + Some(result) } } - Some(()) + value.deserialize(Visitor, diagnostics) } } -impl VisitNode for Complexity { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, +impl Deserializable for Complexity { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "recommended" => { - self.recommended = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "all" => { - self.all = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "noBannedTypes" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noBannedTypes", diagnostics)?; - self.no_banned_types = Some(configuration); - } - "noExcessiveCognitiveComplexity" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noExcessiveCognitiveComplexity", - diagnostics, - )?; - self.no_excessive_cognitive_complexity = Some(configuration); - } - "noExtraBooleanCast" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noExtraBooleanCast", diagnostics)?; - self.no_extra_boolean_cast = Some(configuration); - } - "noForEach" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noForEach", diagnostics)?; - self.no_for_each = Some(configuration); - } - "noMultipleSpacesInRegularExpressionLiterals" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noMultipleSpacesInRegularExpressionLiterals", - diagnostics, - )?; - self.no_multiple_spaces_in_regular_expression_literals = Some(configuration); - } - "noStaticOnlyClass" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noStaticOnlyClass", diagnostics)?; - self.no_static_only_class = Some(configuration); - } - "noUselessCatch" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUselessCatch", diagnostics)?; - self.no_useless_catch = Some(configuration); - } - "noUselessConstructor" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noUselessConstructor", - diagnostics, - )?; - self.no_useless_constructor = Some(configuration); - } - "noUselessEmptyExport" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noUselessEmptyExport", - diagnostics, - )?; - self.no_useless_empty_export = Some(configuration); - } - "noUselessFragments" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUselessFragments", diagnostics)?; - self.no_useless_fragments = Some(configuration); - } - "noUselessLabel" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUselessLabel", diagnostics)?; - self.no_useless_label = Some(configuration); - } - "noUselessRename" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUselessRename", diagnostics)?; - self.no_useless_rename = Some(configuration); - } - "noUselessSwitchCase" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUselessSwitchCase", diagnostics)?; - self.no_useless_switch_case = Some(configuration); - } - "noUselessThisAlias" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUselessThisAlias", diagnostics)?; - self.no_useless_this_alias = Some(configuration); - } - "noUselessTypeConstraint" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noUselessTypeConstraint", - diagnostics, - )?; - self.no_useless_type_constraint = Some(configuration); - } - "noVoid" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noVoid", diagnostics)?; - self.no_void = Some(configuration); - } - "noWith" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noWith", diagnostics)?; - self.no_with = Some(configuration); - } - "useFlatMap" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useFlatMap", diagnostics)?; - self.use_flat_map = Some(configuration); - } - "useLiteralKeys" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useLiteralKeys", diagnostics)?; - self.use_literal_keys = Some(configuration); - } - "useOptionalChain" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useOptionalChain", diagnostics)?; - self.use_optional_chain = Some(configuration); - } - "useSimpleNumberKeys" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useSimpleNumberKeys", diagnostics)?; - self.use_simple_number_keys = Some(configuration); - } - "useSimplifiedLogicExpression" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useSimplifiedLogicExpression", - diagnostics, - )?; - self.use_simplified_logic_expression = Some(configuration); - } - _ => { - report_unknown_map_key( - &name, - &[ - "recommended", - "all", - "noBannedTypes", - "noExcessiveCognitiveComplexity", - "noExtraBooleanCast", - "noForEach", - "noMultipleSpacesInRegularExpressionLiterals", - "noStaticOnlyClass", - "noUselessCatch", - "noUselessConstructor", - "noUselessEmptyExport", - "noUselessFragments", - "noUselessLabel", - "noUselessRename", - "noUselessSwitchCase", - "noUselessThisAlias", - "noUselessTypeConstraint", - "noVoid", - "noWith", - "useFlatMap", - "useLiteralKeys", - "useOptionalChain", - "useSimpleNumberKeys", - "useSimplifiedLogicExpression", - ], - diagnostics, - ); + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = Complexity; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut recommended_is_set = false; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "recommended" => { + recommended_is_set = true; + result.recommended = Deserializable::deserialize(value, diagnostics); + } + "all" => { + result.all = Deserializable::deserialize(value, diagnostics); + } + "noBannedTypes" => { + result.no_banned_types = RuleConfiguration::deserialize_from_rule_name( + "noBannedTypes", + value, + diagnostics, + ); + } + "noExcessiveCognitiveComplexity" => { + result.no_excessive_cognitive_complexity = + RuleConfiguration::deserialize_from_rule_name( + "noExcessiveCognitiveComplexity", + value, + diagnostics, + ); + } + "noExtraBooleanCast" => { + result.no_extra_boolean_cast = + RuleConfiguration::deserialize_from_rule_name( + "noExtraBooleanCast", + value, + diagnostics, + ); + } + "noForEach" => { + result.no_for_each = RuleConfiguration::deserialize_from_rule_name( + "noForEach", + value, + diagnostics, + ); + } + "noMultipleSpacesInRegularExpressionLiterals" => { + result.no_multiple_spaces_in_regular_expression_literals = + RuleConfiguration::deserialize_from_rule_name( + "noMultipleSpacesInRegularExpressionLiterals", + value, + diagnostics, + ); + } + "noStaticOnlyClass" => { + result.no_static_only_class = + RuleConfiguration::deserialize_from_rule_name( + "noStaticOnlyClass", + value, + diagnostics, + ); + } + "noUselessCatch" => { + result.no_useless_catch = RuleConfiguration::deserialize_from_rule_name( + "noUselessCatch", + value, + diagnostics, + ); + } + "noUselessConstructor" => { + result.no_useless_constructor = + RuleConfiguration::deserialize_from_rule_name( + "noUselessConstructor", + value, + diagnostics, + ); + } + "noUselessEmptyExport" => { + result.no_useless_empty_export = + RuleConfiguration::deserialize_from_rule_name( + "noUselessEmptyExport", + value, + diagnostics, + ); + } + "noUselessFragments" => { + result.no_useless_fragments = + RuleConfiguration::deserialize_from_rule_name( + "noUselessFragments", + value, + diagnostics, + ); + } + "noUselessLabel" => { + result.no_useless_label = RuleConfiguration::deserialize_from_rule_name( + "noUselessLabel", + value, + diagnostics, + ); + } + "noUselessRename" => { + result.no_useless_rename = + RuleConfiguration::deserialize_from_rule_name( + "noUselessRename", + value, + diagnostics, + ); + } + "noUselessSwitchCase" => { + result.no_useless_switch_case = + RuleConfiguration::deserialize_from_rule_name( + "noUselessSwitchCase", + value, + diagnostics, + ); + } + "noUselessThisAlias" => { + result.no_useless_this_alias = + RuleConfiguration::deserialize_from_rule_name( + "noUselessThisAlias", + value, + diagnostics, + ); + } + "noUselessTypeConstraint" => { + result.no_useless_type_constraint = + RuleConfiguration::deserialize_from_rule_name( + "noUselessTypeConstraint", + value, + diagnostics, + ); + } + "noVoid" => { + result.no_void = RuleConfiguration::deserialize_from_rule_name( + "noVoid", + value, + diagnostics, + ); + } + "noWith" => { + result.no_with = RuleConfiguration::deserialize_from_rule_name( + "noWith", + value, + diagnostics, + ); + } + "useFlatMap" => { + result.use_flat_map = RuleConfiguration::deserialize_from_rule_name( + "useFlatMap", + value, + diagnostics, + ); + } + "useLiteralKeys" => { + result.use_literal_keys = RuleConfiguration::deserialize_from_rule_name( + "useLiteralKeys", + value, + diagnostics, + ); + } + "useOptionalChain" => { + result.use_optional_chain = + RuleConfiguration::deserialize_from_rule_name( + "useOptionalChain", + value, + diagnostics, + ); + } + "useSimpleNumberKeys" => { + result.use_simple_number_keys = + RuleConfiguration::deserialize_from_rule_name( + "useSimpleNumberKeys", + value, + diagnostics, + ); + } + "useSimplifiedLogicExpression" => { + result.use_simplified_logic_expression = + RuleConfiguration::deserialize_from_rule_name( + "useSimplifiedLogicExpression", + value, + diagnostics, + ); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + &[ + "recommended", + "all", + "noBannedTypes", + "noExcessiveCognitiveComplexity", + "noExtraBooleanCast", + "noForEach", + "noMultipleSpacesInRegularExpressionLiterals", + "noStaticOnlyClass", + "noUselessCatch", + "noUselessConstructor", + "noUselessEmptyExport", + "noUselessFragments", + "noUselessLabel", + "noUselessRename", + "noUselessSwitchCase", + "noUselessThisAlias", + "noUselessTypeConstraint", + "noVoid", + "noWith", + "useFlatMap", + "useLiteralKeys", + "useOptionalChain", + "useSimpleNumberKeys", + "useSimplifiedLogicExpression", + ], + )); + } + } + } + if recommended_is_set + && matches!(result.recommended, Some(true)) + && matches!(result.all, Some(true)) + { + diagnostics . push (DeserializationDiagnostic :: new (markup ! (< Emphasis > "'recommended'" < / Emphasis > " and " < Emphasis > "'all'" < / Emphasis > " can't be both " < Emphasis > "'true'" < / Emphasis > ". You should choose only one of them.")) . with_range (range) . with_note (markup ! ("Biome will fallback to its defaults for this section."))) ; + return Some(Self::Output::default()); + } + Some(result) } } - Some(()) + value.deserialize(Visitor, diagnostics) } } -impl VisitNode for Correctness { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, +impl Deserializable for Correctness { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "recommended" => { - self.recommended = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "all" => { - self.all = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "noChildrenProp" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noChildrenProp", diagnostics)?; - self.no_children_prop = Some(configuration); - } - "noConstAssign" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noConstAssign", diagnostics)?; - self.no_const_assign = Some(configuration); - } - "noConstantCondition" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noConstantCondition", diagnostics)?; - self.no_constant_condition = Some(configuration); - } - "noConstructorReturn" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noConstructorReturn", diagnostics)?; - self.no_constructor_return = Some(configuration); - } - "noEmptyPattern" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noEmptyPattern", diagnostics)?; - self.no_empty_pattern = Some(configuration); - } - "noGlobalObjectCalls" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noGlobalObjectCalls", diagnostics)?; - self.no_global_object_calls = Some(configuration); - } - "noInnerDeclarations" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noInnerDeclarations", diagnostics)?; - self.no_inner_declarations = Some(configuration); - } - "noInvalidConstructorSuper" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noInvalidConstructorSuper", - diagnostics, - )?; - self.no_invalid_constructor_super = Some(configuration); - } - "noNewSymbol" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noNewSymbol", diagnostics)?; - self.no_new_symbol = Some(configuration); - } - "noNonoctalDecimalEscape" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noNonoctalDecimalEscape", - diagnostics, - )?; - self.no_nonoctal_decimal_escape = Some(configuration); - } - "noPrecisionLoss" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noPrecisionLoss", diagnostics)?; - self.no_precision_loss = Some(configuration); - } - "noRenderReturnValue" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noRenderReturnValue", diagnostics)?; - self.no_render_return_value = Some(configuration); - } - "noSelfAssign" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noSelfAssign", diagnostics)?; - self.no_self_assign = Some(configuration); - } - "noSetterReturn" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noSetterReturn", diagnostics)?; - self.no_setter_return = Some(configuration); - } - "noStringCaseMismatch" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noStringCaseMismatch", - diagnostics, - )?; - self.no_string_case_mismatch = Some(configuration); - } - "noSwitchDeclarations" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noSwitchDeclarations", - diagnostics, - )?; - self.no_switch_declarations = Some(configuration); - } - "noUndeclaredVariables" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noUndeclaredVariables", - diagnostics, - )?; - self.no_undeclared_variables = Some(configuration); - } - "noUnnecessaryContinue" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noUnnecessaryContinue", - diagnostics, - )?; - self.no_unnecessary_continue = Some(configuration); - } - "noUnreachable" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUnreachable", diagnostics)?; - self.no_unreachable = Some(configuration); - } - "noUnreachableSuper" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUnreachableSuper", diagnostics)?; - self.no_unreachable_super = Some(configuration); - } - "noUnsafeFinally" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUnsafeFinally", diagnostics)?; - self.no_unsafe_finally = Some(configuration); - } - "noUnsafeOptionalChaining" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noUnsafeOptionalChaining", - diagnostics, - )?; - self.no_unsafe_optional_chaining = Some(configuration); - } - "noUnusedLabels" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUnusedLabels", diagnostics)?; - self.no_unused_labels = Some(configuration); - } - "noUnusedVariables" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUnusedVariables", diagnostics)?; - self.no_unused_variables = Some(configuration); - } - "noVoidElementsWithChildren" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noVoidElementsWithChildren", - diagnostics, - )?; - self.no_void_elements_with_children = Some(configuration); - } - "noVoidTypeReturn" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noVoidTypeReturn", diagnostics)?; - self.no_void_type_return = Some(configuration); - } - "useExhaustiveDependencies" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useExhaustiveDependencies", - diagnostics, - )?; - self.use_exhaustive_dependencies = Some(configuration); - } - "useHookAtTopLevel" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useHookAtTopLevel", diagnostics)?; - self.use_hook_at_top_level = Some(configuration); - } - "useIsNan" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useIsNan", diagnostics)?; - self.use_is_nan = Some(configuration); - } - "useValidForDirection" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useValidForDirection", - diagnostics, - )?; - self.use_valid_for_direction = Some(configuration); - } - "useYield" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useYield", diagnostics)?; - self.use_yield = Some(configuration); - } - _ => { - report_unknown_map_key( - &name, - &[ - "recommended", - "all", - "noChildrenProp", - "noConstAssign", - "noConstantCondition", - "noConstructorReturn", - "noEmptyPattern", - "noGlobalObjectCalls", - "noInnerDeclarations", - "noInvalidConstructorSuper", - "noNewSymbol", - "noNonoctalDecimalEscape", - "noPrecisionLoss", - "noRenderReturnValue", - "noSelfAssign", - "noSetterReturn", - "noStringCaseMismatch", - "noSwitchDeclarations", - "noUndeclaredVariables", - "noUnnecessaryContinue", - "noUnreachable", - "noUnreachableSuper", - "noUnsafeFinally", - "noUnsafeOptionalChaining", - "noUnusedLabels", - "noUnusedVariables", - "noVoidElementsWithChildren", - "noVoidTypeReturn", - "useExhaustiveDependencies", - "useHookAtTopLevel", - "useIsNan", - "useValidForDirection", - "useYield", - ], - diagnostics, - ); + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = Correctness; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut recommended_is_set = false; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "recommended" => { + recommended_is_set = true; + result.recommended = Deserializable::deserialize(value, diagnostics); + } + "all" => { + result.all = Deserializable::deserialize(value, diagnostics); + } + "noChildrenProp" => { + result.no_children_prop = RuleConfiguration::deserialize_from_rule_name( + "noChildrenProp", + value, + diagnostics, + ); + } + "noConstAssign" => { + result.no_const_assign = RuleConfiguration::deserialize_from_rule_name( + "noConstAssign", + value, + diagnostics, + ); + } + "noConstantCondition" => { + result.no_constant_condition = + RuleConfiguration::deserialize_from_rule_name( + "noConstantCondition", + value, + diagnostics, + ); + } + "noConstructorReturn" => { + result.no_constructor_return = + RuleConfiguration::deserialize_from_rule_name( + "noConstructorReturn", + value, + diagnostics, + ); + } + "noEmptyPattern" => { + result.no_empty_pattern = RuleConfiguration::deserialize_from_rule_name( + "noEmptyPattern", + value, + diagnostics, + ); + } + "noGlobalObjectCalls" => { + result.no_global_object_calls = + RuleConfiguration::deserialize_from_rule_name( + "noGlobalObjectCalls", + value, + diagnostics, + ); + } + "noInnerDeclarations" => { + result.no_inner_declarations = + RuleConfiguration::deserialize_from_rule_name( + "noInnerDeclarations", + value, + diagnostics, + ); + } + "noInvalidConstructorSuper" => { + result.no_invalid_constructor_super = + RuleConfiguration::deserialize_from_rule_name( + "noInvalidConstructorSuper", + value, + diagnostics, + ); + } + "noNewSymbol" => { + result.no_new_symbol = RuleConfiguration::deserialize_from_rule_name( + "noNewSymbol", + value, + diagnostics, + ); + } + "noNonoctalDecimalEscape" => { + result.no_nonoctal_decimal_escape = + RuleConfiguration::deserialize_from_rule_name( + "noNonoctalDecimalEscape", + value, + diagnostics, + ); + } + "noPrecisionLoss" => { + result.no_precision_loss = + RuleConfiguration::deserialize_from_rule_name( + "noPrecisionLoss", + value, + diagnostics, + ); + } + "noRenderReturnValue" => { + result.no_render_return_value = + RuleConfiguration::deserialize_from_rule_name( + "noRenderReturnValue", + value, + diagnostics, + ); + } + "noSelfAssign" => { + result.no_self_assign = RuleConfiguration::deserialize_from_rule_name( + "noSelfAssign", + value, + diagnostics, + ); + } + "noSetterReturn" => { + result.no_setter_return = RuleConfiguration::deserialize_from_rule_name( + "noSetterReturn", + value, + diagnostics, + ); + } + "noStringCaseMismatch" => { + result.no_string_case_mismatch = + RuleConfiguration::deserialize_from_rule_name( + "noStringCaseMismatch", + value, + diagnostics, + ); + } + "noSwitchDeclarations" => { + result.no_switch_declarations = + RuleConfiguration::deserialize_from_rule_name( + "noSwitchDeclarations", + value, + diagnostics, + ); + } + "noUndeclaredVariables" => { + result.no_undeclared_variables = + RuleConfiguration::deserialize_from_rule_name( + "noUndeclaredVariables", + value, + diagnostics, + ); + } + "noUnnecessaryContinue" => { + result.no_unnecessary_continue = + RuleConfiguration::deserialize_from_rule_name( + "noUnnecessaryContinue", + value, + diagnostics, + ); + } + "noUnreachable" => { + result.no_unreachable = RuleConfiguration::deserialize_from_rule_name( + "noUnreachable", + value, + diagnostics, + ); + } + "noUnreachableSuper" => { + result.no_unreachable_super = + RuleConfiguration::deserialize_from_rule_name( + "noUnreachableSuper", + value, + diagnostics, + ); + } + "noUnsafeFinally" => { + result.no_unsafe_finally = + RuleConfiguration::deserialize_from_rule_name( + "noUnsafeFinally", + value, + diagnostics, + ); + } + "noUnsafeOptionalChaining" => { + result.no_unsafe_optional_chaining = + RuleConfiguration::deserialize_from_rule_name( + "noUnsafeOptionalChaining", + value, + diagnostics, + ); + } + "noUnusedLabels" => { + result.no_unused_labels = RuleConfiguration::deserialize_from_rule_name( + "noUnusedLabels", + value, + diagnostics, + ); + } + "noUnusedVariables" => { + result.no_unused_variables = + RuleConfiguration::deserialize_from_rule_name( + "noUnusedVariables", + value, + diagnostics, + ); + } + "noVoidElementsWithChildren" => { + result.no_void_elements_with_children = + RuleConfiguration::deserialize_from_rule_name( + "noVoidElementsWithChildren", + value, + diagnostics, + ); + } + "noVoidTypeReturn" => { + result.no_void_type_return = + RuleConfiguration::deserialize_from_rule_name( + "noVoidTypeReturn", + value, + diagnostics, + ); + } + "useExhaustiveDependencies" => { + result.use_exhaustive_dependencies = + RuleConfiguration::deserialize_from_rule_name( + "useExhaustiveDependencies", + value, + diagnostics, + ); + } + "useHookAtTopLevel" => { + result.use_hook_at_top_level = + RuleConfiguration::deserialize_from_rule_name( + "useHookAtTopLevel", + value, + diagnostics, + ); + } + "useIsNan" => { + result.use_is_nan = RuleConfiguration::deserialize_from_rule_name( + "useIsNan", + value, + diagnostics, + ); + } + "useValidForDirection" => { + result.use_valid_for_direction = + RuleConfiguration::deserialize_from_rule_name( + "useValidForDirection", + value, + diagnostics, + ); + } + "useYield" => { + result.use_yield = RuleConfiguration::deserialize_from_rule_name( + "useYield", + value, + diagnostics, + ); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + &[ + "recommended", + "all", + "noChildrenProp", + "noConstAssign", + "noConstantCondition", + "noConstructorReturn", + "noEmptyPattern", + "noGlobalObjectCalls", + "noInnerDeclarations", + "noInvalidConstructorSuper", + "noNewSymbol", + "noNonoctalDecimalEscape", + "noPrecisionLoss", + "noRenderReturnValue", + "noSelfAssign", + "noSetterReturn", + "noStringCaseMismatch", + "noSwitchDeclarations", + "noUndeclaredVariables", + "noUnnecessaryContinue", + "noUnreachable", + "noUnreachableSuper", + "noUnsafeFinally", + "noUnsafeOptionalChaining", + "noUnusedLabels", + "noUnusedVariables", + "noVoidElementsWithChildren", + "noVoidTypeReturn", + "useExhaustiveDependencies", + "useHookAtTopLevel", + "useIsNan", + "useValidForDirection", + "useYield", + ], + )); + } + } + } + if recommended_is_set + && matches!(result.recommended, Some(true)) + && matches!(result.all, Some(true)) + { + diagnostics . push (DeserializationDiagnostic :: new (markup ! (< Emphasis > "'recommended'" < / Emphasis > " and " < Emphasis > "'all'" < / Emphasis > " can't be both " < Emphasis > "'true'" < / Emphasis > ". You should choose only one of them.")) . with_range (range) . with_note (markup ! ("Biome will fallback to its defaults for this section."))) ; + return Some(Self::Output::default()); + } + Some(result) } } - Some(()) + value.deserialize(Visitor, diagnostics) } } -impl VisitNode for Nursery { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, +impl Deserializable for Nursery { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "recommended" => { - self.recommended = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "all" => { - self.all = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "noApproximativeNumericConstant" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noApproximativeNumericConstant", - diagnostics, - )?; - self.no_approximative_numeric_constant = Some(configuration); - } - "noDuplicateJsonKeys" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noDuplicateJsonKeys", diagnostics)?; - self.no_duplicate_json_keys = Some(configuration); - } - "noEmptyBlockStatements" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noEmptyBlockStatements", - diagnostics, - )?; - self.no_empty_block_statements = Some(configuration); - } - "noEmptyCharacterClassInRegex" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noEmptyCharacterClassInRegex", - diagnostics, - )?; - self.no_empty_character_class_in_regex = Some(configuration); - } - "noInteractiveElementToNoninteractiveRole" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noInteractiveElementToNoninteractiveRole", - diagnostics, - )?; - self.no_interactive_element_to_noninteractive_role = Some(configuration); - } - "noInvalidNewBuiltin" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noInvalidNewBuiltin", diagnostics)?; - self.no_invalid_new_builtin = Some(configuration); - } - "noMisleadingInstantiator" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noMisleadingInstantiator", - diagnostics, - )?; - self.no_misleading_instantiator = Some(configuration); - } - "noMisrefactoredShorthandAssign" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noMisrefactoredShorthandAssign", - diagnostics, - )?; - self.no_misrefactored_shorthand_assign = Some(configuration); - } - "noThisInStatic" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noThisInStatic", diagnostics)?; - self.no_this_in_static = Some(configuration); - } - "noUnusedImports" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUnusedImports", diagnostics)?; - self.no_unused_imports = Some(configuration); - } - "noUselessElse" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUselessElse", diagnostics)?; - self.no_useless_else = Some(configuration); - } - "noUselessLoneBlockStatements" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noUselessLoneBlockStatements", - diagnostics, - )?; - self.no_useless_lone_block_statements = Some(configuration); - } - "useAriaActivedescendantWithTabindex" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useAriaActivedescendantWithTabindex", - diagnostics, - )?; - self.use_aria_activedescendant_with_tabindex = Some(configuration); - } - "useArrowFunction" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useArrowFunction", diagnostics)?; - self.use_arrow_function = Some(configuration); - } - "useAsConstAssertion" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useAsConstAssertion", diagnostics)?; - self.use_as_const_assertion = Some(configuration); - } - "useGroupedTypeImport" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useGroupedTypeImport", - diagnostics, - )?; - self.use_grouped_type_import = Some(configuration); - } - "useImportRestrictions" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useImportRestrictions", - diagnostics, - )?; - self.use_import_restrictions = Some(configuration); - } - "useShorthandAssign" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useShorthandAssign", diagnostics)?; - self.use_shorthand_assign = Some(configuration); - } - _ => { - report_unknown_map_key( - &name, - &[ - "recommended", - "all", - "noApproximativeNumericConstant", - "noDuplicateJsonKeys", - "noEmptyBlockStatements", - "noEmptyCharacterClassInRegex", - "noInteractiveElementToNoninteractiveRole", - "noInvalidNewBuiltin", - "noMisleadingInstantiator", - "noMisrefactoredShorthandAssign", - "noThisInStatic", - "noUnusedImports", - "noUselessElse", - "noUselessLoneBlockStatements", - "useAriaActivedescendantWithTabindex", - "useArrowFunction", - "useAsConstAssertion", - "useGroupedTypeImport", - "useImportRestrictions", - "useShorthandAssign", - ], - diagnostics, - ); + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = Nursery; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut recommended_is_set = false; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "recommended" => { + recommended_is_set = true; + result.recommended = Deserializable::deserialize(value, diagnostics); + } + "all" => { + result.all = Deserializable::deserialize(value, diagnostics); + } + "noApproximativeNumericConstant" => { + result.no_approximative_numeric_constant = + RuleConfiguration::deserialize_from_rule_name( + "noApproximativeNumericConstant", + value, + diagnostics, + ); + } + "noDuplicateJsonKeys" => { + result.no_duplicate_json_keys = + RuleConfiguration::deserialize_from_rule_name( + "noDuplicateJsonKeys", + value, + diagnostics, + ); + } + "noEmptyBlockStatements" => { + result.no_empty_block_statements = + RuleConfiguration::deserialize_from_rule_name( + "noEmptyBlockStatements", + value, + diagnostics, + ); + } + "noEmptyCharacterClassInRegex" => { + result.no_empty_character_class_in_regex = + RuleConfiguration::deserialize_from_rule_name( + "noEmptyCharacterClassInRegex", + value, + diagnostics, + ); + } + "noInteractiveElementToNoninteractiveRole" => { + result.no_interactive_element_to_noninteractive_role = + RuleConfiguration::deserialize_from_rule_name( + "noInteractiveElementToNoninteractiveRole", + value, + diagnostics, + ); + } + "noInvalidNewBuiltin" => { + result.no_invalid_new_builtin = + RuleConfiguration::deserialize_from_rule_name( + "noInvalidNewBuiltin", + value, + diagnostics, + ); + } + "noMisleadingInstantiator" => { + result.no_misleading_instantiator = + RuleConfiguration::deserialize_from_rule_name( + "noMisleadingInstantiator", + value, + diagnostics, + ); + } + "noMisrefactoredShorthandAssign" => { + result.no_misrefactored_shorthand_assign = + RuleConfiguration::deserialize_from_rule_name( + "noMisrefactoredShorthandAssign", + value, + diagnostics, + ); + } + "noThisInStatic" => { + result.no_this_in_static = + RuleConfiguration::deserialize_from_rule_name( + "noThisInStatic", + value, + diagnostics, + ); + } + "noUnusedImports" => { + result.no_unused_imports = + RuleConfiguration::deserialize_from_rule_name( + "noUnusedImports", + value, + diagnostics, + ); + } + "noUselessElse" => { + result.no_useless_else = RuleConfiguration::deserialize_from_rule_name( + "noUselessElse", + value, + diagnostics, + ); + } + "noUselessLoneBlockStatements" => { + result.no_useless_lone_block_statements = + RuleConfiguration::deserialize_from_rule_name( + "noUselessLoneBlockStatements", + value, + diagnostics, + ); + } + "useAriaActivedescendantWithTabindex" => { + result.use_aria_activedescendant_with_tabindex = + RuleConfiguration::deserialize_from_rule_name( + "useAriaActivedescendantWithTabindex", + value, + diagnostics, + ); + } + "useArrowFunction" => { + result.use_arrow_function = + RuleConfiguration::deserialize_from_rule_name( + "useArrowFunction", + value, + diagnostics, + ); + } + "useAsConstAssertion" => { + result.use_as_const_assertion = + RuleConfiguration::deserialize_from_rule_name( + "useAsConstAssertion", + value, + diagnostics, + ); + } + "useGroupedTypeImport" => { + result.use_grouped_type_import = + RuleConfiguration::deserialize_from_rule_name( + "useGroupedTypeImport", + value, + diagnostics, + ); + } + "useImportRestrictions" => { + result.use_import_restrictions = + RuleConfiguration::deserialize_from_rule_name( + "useImportRestrictions", + value, + diagnostics, + ); + } + "useShorthandAssign" => { + result.use_shorthand_assign = + RuleConfiguration::deserialize_from_rule_name( + "useShorthandAssign", + value, + diagnostics, + ); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + &[ + "recommended", + "all", + "noApproximativeNumericConstant", + "noDuplicateJsonKeys", + "noEmptyBlockStatements", + "noEmptyCharacterClassInRegex", + "noInteractiveElementToNoninteractiveRole", + "noInvalidNewBuiltin", + "noMisleadingInstantiator", + "noMisrefactoredShorthandAssign", + "noThisInStatic", + "noUnusedImports", + "noUselessElse", + "noUselessLoneBlockStatements", + "useAriaActivedescendantWithTabindex", + "useArrowFunction", + "useAsConstAssertion", + "useGroupedTypeImport", + "useImportRestrictions", + "useShorthandAssign", + ], + )); + } + } + } + if recommended_is_set + && matches!(result.recommended, Some(true)) + && matches!(result.all, Some(true)) + { + diagnostics . push (DeserializationDiagnostic :: new (markup ! (< Emphasis > "'recommended'" < / Emphasis > " and " < Emphasis > "'all'" < / Emphasis > " can't be both " < Emphasis > "'true'" < / Emphasis > ". You should choose only one of them.")) . with_range (range) . with_note (markup ! ("Biome will fallback to its defaults for this section."))) ; + return Some(Self::Output::default()); + } + Some(result) } } - Some(()) + value.deserialize(Visitor, diagnostics) } } -impl VisitNode for Performance { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, +impl Deserializable for Performance { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "recommended" => { - self.recommended = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "all" => { - self.all = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "noAccumulatingSpread" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noAccumulatingSpread", - diagnostics, - )?; - self.no_accumulating_spread = Some(configuration); - } - "noDelete" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noDelete", diagnostics)?; - self.no_delete = Some(configuration); - } - _ => { - report_unknown_map_key( - &name, - &["recommended", "all", "noAccumulatingSpread", "noDelete"], - diagnostics, - ); + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = Performance; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut recommended_is_set = false; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "recommended" => { + recommended_is_set = true; + result.recommended = Deserializable::deserialize(value, diagnostics); + } + "all" => { + result.all = Deserializable::deserialize(value, diagnostics); + } + "noAccumulatingSpread" => { + result.no_accumulating_spread = + RuleConfiguration::deserialize_from_rule_name( + "noAccumulatingSpread", + value, + diagnostics, + ); + } + "noDelete" => { + result.no_delete = RuleConfiguration::deserialize_from_rule_name( + "noDelete", + value, + diagnostics, + ); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + &["recommended", "all", "noAccumulatingSpread", "noDelete"], + )); + } + } + } + if recommended_is_set + && matches!(result.recommended, Some(true)) + && matches!(result.all, Some(true)) + { + diagnostics . push (DeserializationDiagnostic :: new (markup ! (< Emphasis > "'recommended'" < / Emphasis > " and " < Emphasis > "'all'" < / Emphasis > " can't be both " < Emphasis > "'true'" < / Emphasis > ". You should choose only one of them.")) . with_range (range) . with_note (markup ! ("Biome will fallback to its defaults for this section."))) ; + return Some(Self::Output::default()); + } + Some(result) } } - Some(()) + value.deserialize(Visitor, diagnostics) } } -impl VisitNode for Security { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, +impl Deserializable for Security { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "recommended" => { - self.recommended = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "all" => { - self.all = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "noDangerouslySetInnerHtml" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noDangerouslySetInnerHtml", - diagnostics, - )?; - self.no_dangerously_set_inner_html = Some(configuration); - } - "noDangerouslySetInnerHtmlWithChildren" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noDangerouslySetInnerHtmlWithChildren", - diagnostics, - )?; - self.no_dangerously_set_inner_html_with_children = Some(configuration); - } - _ => { - report_unknown_map_key( - &name, - &[ - "recommended", - "all", - "noDangerouslySetInnerHtml", - "noDangerouslySetInnerHtmlWithChildren", - ], - diagnostics, - ); + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = Security; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut recommended_is_set = false; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "recommended" => { + recommended_is_set = true; + result.recommended = Deserializable::deserialize(value, diagnostics); + } + "all" => { + result.all = Deserializable::deserialize(value, diagnostics); + } + "noDangerouslySetInnerHtml" => { + result.no_dangerously_set_inner_html = + RuleConfiguration::deserialize_from_rule_name( + "noDangerouslySetInnerHtml", + value, + diagnostics, + ); + } + "noDangerouslySetInnerHtmlWithChildren" => { + result.no_dangerously_set_inner_html_with_children = + RuleConfiguration::deserialize_from_rule_name( + "noDangerouslySetInnerHtmlWithChildren", + value, + diagnostics, + ); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + &[ + "recommended", + "all", + "noDangerouslySetInnerHtml", + "noDangerouslySetInnerHtmlWithChildren", + ], + )); + } + } + } + if recommended_is_set + && matches!(result.recommended, Some(true)) + && matches!(result.all, Some(true)) + { + diagnostics . push (DeserializationDiagnostic :: new (markup ! (< Emphasis > "'recommended'" < / Emphasis > " and " < Emphasis > "'all'" < / Emphasis > " can't be both " < Emphasis > "'true'" < / Emphasis > ". You should choose only one of them.")) . with_range (range) . with_note (markup ! ("Biome will fallback to its defaults for this section."))) ; + return Some(Self::Output::default()); + } + Some(result) } } - Some(()) + value.deserialize(Visitor, diagnostics) } } -impl VisitNode for Style { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, +impl Deserializable for Style { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "recommended" => { - self.recommended = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "all" => { - self.all = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "noArguments" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noArguments", diagnostics)?; - self.no_arguments = Some(configuration); - } - "noCommaOperator" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noCommaOperator", diagnostics)?; - self.no_comma_operator = Some(configuration); - } - "noImplicitBoolean" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noImplicitBoolean", diagnostics)?; - self.no_implicit_boolean = Some(configuration); - } - "noInferrableTypes" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noInferrableTypes", diagnostics)?; - self.no_inferrable_types = Some(configuration); - } - "noNamespace" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noNamespace", diagnostics)?; - self.no_namespace = Some(configuration); - } - "noNegationElse" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noNegationElse", diagnostics)?; - self.no_negation_else = Some(configuration); - } - "noNonNullAssertion" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noNonNullAssertion", diagnostics)?; - self.no_non_null_assertion = Some(configuration); - } - "noParameterAssign" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noParameterAssign", diagnostics)?; - self.no_parameter_assign = Some(configuration); - } - "noParameterProperties" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noParameterProperties", - diagnostics, - )?; - self.no_parameter_properties = Some(configuration); - } - "noRestrictedGlobals" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noRestrictedGlobals", diagnostics)?; - self.no_restricted_globals = Some(configuration); - } - "noShoutyConstants" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noShoutyConstants", diagnostics)?; - self.no_shouty_constants = Some(configuration); - } - "noUnusedTemplateLiteral" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noUnusedTemplateLiteral", - diagnostics, - )?; - self.no_unused_template_literal = Some(configuration); - } - "noVar" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noVar", diagnostics)?; - self.no_var = Some(configuration); - } - "useBlockStatements" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useBlockStatements", diagnostics)?; - self.use_block_statements = Some(configuration); - } - "useCollapsedElseIf" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useCollapsedElseIf", diagnostics)?; - self.use_collapsed_else_if = Some(configuration); - } - "useConst" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useConst", diagnostics)?; - self.use_const = Some(configuration); - } - "useDefaultParameterLast" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useDefaultParameterLast", - diagnostics, - )?; - self.use_default_parameter_last = Some(configuration); - } - "useEnumInitializers" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useEnumInitializers", diagnostics)?; - self.use_enum_initializers = Some(configuration); - } - "useExponentiationOperator" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useExponentiationOperator", - diagnostics, - )?; - self.use_exponentiation_operator = Some(configuration); - } - "useFragmentSyntax" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useFragmentSyntax", diagnostics)?; - self.use_fragment_syntax = Some(configuration); - } - "useLiteralEnumMembers" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useLiteralEnumMembers", - diagnostics, - )?; - self.use_literal_enum_members = Some(configuration); - } - "useNamingConvention" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useNamingConvention", diagnostics)?; - self.use_naming_convention = Some(configuration); - } - "useNumericLiterals" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useNumericLiterals", diagnostics)?; - self.use_numeric_literals = Some(configuration); - } - "useSelfClosingElements" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useSelfClosingElements", - diagnostics, - )?; - self.use_self_closing_elements = Some(configuration); - } - "useShorthandArrayType" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useShorthandArrayType", - diagnostics, - )?; - self.use_shorthand_array_type = Some(configuration); - } - "useSingleCaseStatement" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useSingleCaseStatement", - diagnostics, - )?; - self.use_single_case_statement = Some(configuration); - } - "useSingleVarDeclarator" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useSingleVarDeclarator", - diagnostics, - )?; - self.use_single_var_declarator = Some(configuration); - } - "useTemplate" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useTemplate", diagnostics)?; - self.use_template = Some(configuration); - } - "useWhile" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useWhile", diagnostics)?; - self.use_while = Some(configuration); - } - _ => { - report_unknown_map_key( - &name, - &[ - "recommended", - "all", - "noArguments", - "noCommaOperator", - "noImplicitBoolean", - "noInferrableTypes", - "noNamespace", - "noNegationElse", - "noNonNullAssertion", - "noParameterAssign", - "noParameterProperties", - "noRestrictedGlobals", - "noShoutyConstants", - "noUnusedTemplateLiteral", - "noVar", - "useBlockStatements", - "useCollapsedElseIf", - "useConst", - "useDefaultParameterLast", - "useEnumInitializers", - "useExponentiationOperator", - "useFragmentSyntax", - "useLiteralEnumMembers", - "useNamingConvention", - "useNumericLiterals", - "useSelfClosingElements", - "useShorthandArrayType", - "useSingleCaseStatement", - "useSingleVarDeclarator", - "useTemplate", - "useWhile", - ], - diagnostics, - ); + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = Style; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut recommended_is_set = false; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "recommended" => { + recommended_is_set = true; + result.recommended = Deserializable::deserialize(value, diagnostics); + } + "all" => { + result.all = Deserializable::deserialize(value, diagnostics); + } + "noArguments" => { + result.no_arguments = RuleConfiguration::deserialize_from_rule_name( + "noArguments", + value, + diagnostics, + ); + } + "noCommaOperator" => { + result.no_comma_operator = + RuleConfiguration::deserialize_from_rule_name( + "noCommaOperator", + value, + diagnostics, + ); + } + "noImplicitBoolean" => { + result.no_implicit_boolean = + RuleConfiguration::deserialize_from_rule_name( + "noImplicitBoolean", + value, + diagnostics, + ); + } + "noInferrableTypes" => { + result.no_inferrable_types = + RuleConfiguration::deserialize_from_rule_name( + "noInferrableTypes", + value, + diagnostics, + ); + } + "noNamespace" => { + result.no_namespace = RuleConfiguration::deserialize_from_rule_name( + "noNamespace", + value, + diagnostics, + ); + } + "noNegationElse" => { + result.no_negation_else = RuleConfiguration::deserialize_from_rule_name( + "noNegationElse", + value, + diagnostics, + ); + } + "noNonNullAssertion" => { + result.no_non_null_assertion = + RuleConfiguration::deserialize_from_rule_name( + "noNonNullAssertion", + value, + diagnostics, + ); + } + "noParameterAssign" => { + result.no_parameter_assign = + RuleConfiguration::deserialize_from_rule_name( + "noParameterAssign", + value, + diagnostics, + ); + } + "noParameterProperties" => { + result.no_parameter_properties = + RuleConfiguration::deserialize_from_rule_name( + "noParameterProperties", + value, + diagnostics, + ); + } + "noRestrictedGlobals" => { + result.no_restricted_globals = + RuleConfiguration::deserialize_from_rule_name( + "noRestrictedGlobals", + value, + diagnostics, + ); + } + "noShoutyConstants" => { + result.no_shouty_constants = + RuleConfiguration::deserialize_from_rule_name( + "noShoutyConstants", + value, + diagnostics, + ); + } + "noUnusedTemplateLiteral" => { + result.no_unused_template_literal = + RuleConfiguration::deserialize_from_rule_name( + "noUnusedTemplateLiteral", + value, + diagnostics, + ); + } + "noVar" => { + result.no_var = RuleConfiguration::deserialize_from_rule_name( + "noVar", + value, + diagnostics, + ); + } + "useBlockStatements" => { + result.use_block_statements = + RuleConfiguration::deserialize_from_rule_name( + "useBlockStatements", + value, + diagnostics, + ); + } + "useCollapsedElseIf" => { + result.use_collapsed_else_if = + RuleConfiguration::deserialize_from_rule_name( + "useCollapsedElseIf", + value, + diagnostics, + ); + } + "useConst" => { + result.use_const = RuleConfiguration::deserialize_from_rule_name( + "useConst", + value, + diagnostics, + ); + } + "useDefaultParameterLast" => { + result.use_default_parameter_last = + RuleConfiguration::deserialize_from_rule_name( + "useDefaultParameterLast", + value, + diagnostics, + ); + } + "useEnumInitializers" => { + result.use_enum_initializers = + RuleConfiguration::deserialize_from_rule_name( + "useEnumInitializers", + value, + diagnostics, + ); + } + "useExponentiationOperator" => { + result.use_exponentiation_operator = + RuleConfiguration::deserialize_from_rule_name( + "useExponentiationOperator", + value, + diagnostics, + ); + } + "useFragmentSyntax" => { + result.use_fragment_syntax = + RuleConfiguration::deserialize_from_rule_name( + "useFragmentSyntax", + value, + diagnostics, + ); + } + "useLiteralEnumMembers" => { + result.use_literal_enum_members = + RuleConfiguration::deserialize_from_rule_name( + "useLiteralEnumMembers", + value, + diagnostics, + ); + } + "useNamingConvention" => { + result.use_naming_convention = + RuleConfiguration::deserialize_from_rule_name( + "useNamingConvention", + value, + diagnostics, + ); + } + "useNumericLiterals" => { + result.use_numeric_literals = + RuleConfiguration::deserialize_from_rule_name( + "useNumericLiterals", + value, + diagnostics, + ); + } + "useSelfClosingElements" => { + result.use_self_closing_elements = + RuleConfiguration::deserialize_from_rule_name( + "useSelfClosingElements", + value, + diagnostics, + ); + } + "useShorthandArrayType" => { + result.use_shorthand_array_type = + RuleConfiguration::deserialize_from_rule_name( + "useShorthandArrayType", + value, + diagnostics, + ); + } + "useSingleCaseStatement" => { + result.use_single_case_statement = + RuleConfiguration::deserialize_from_rule_name( + "useSingleCaseStatement", + value, + diagnostics, + ); + } + "useSingleVarDeclarator" => { + result.use_single_var_declarator = + RuleConfiguration::deserialize_from_rule_name( + "useSingleVarDeclarator", + value, + diagnostics, + ); + } + "useTemplate" => { + result.use_template = RuleConfiguration::deserialize_from_rule_name( + "useTemplate", + value, + diagnostics, + ); + } + "useWhile" => { + result.use_while = RuleConfiguration::deserialize_from_rule_name( + "useWhile", + value, + diagnostics, + ); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + &[ + "recommended", + "all", + "noArguments", + "noCommaOperator", + "noImplicitBoolean", + "noInferrableTypes", + "noNamespace", + "noNegationElse", + "noNonNullAssertion", + "noParameterAssign", + "noParameterProperties", + "noRestrictedGlobals", + "noShoutyConstants", + "noUnusedTemplateLiteral", + "noVar", + "useBlockStatements", + "useCollapsedElseIf", + "useConst", + "useDefaultParameterLast", + "useEnumInitializers", + "useExponentiationOperator", + "useFragmentSyntax", + "useLiteralEnumMembers", + "useNamingConvention", + "useNumericLiterals", + "useSelfClosingElements", + "useShorthandArrayType", + "useSingleCaseStatement", + "useSingleVarDeclarator", + "useTemplate", + "useWhile", + ], + )); + } + } + } + if recommended_is_set + && matches!(result.recommended, Some(true)) + && matches!(result.all, Some(true)) + { + diagnostics . push (DeserializationDiagnostic :: new (markup ! (< Emphasis > "'recommended'" < / Emphasis > " and " < Emphasis > "'all'" < / Emphasis > " can't be both " < Emphasis > "'true'" < / Emphasis > ". You should choose only one of them.")) . with_range (range) . with_note (markup ! ("Biome will fallback to its defaults for this section."))) ; + return Some(Self::Output::default()); + } + Some(result) } } - Some(()) + value.deserialize(Visitor, diagnostics) } } -impl VisitNode for Suspicious { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, +impl Deserializable for Suspicious { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "recommended" => { - self.recommended = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "all" => { - self.all = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - "noArrayIndexKey" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noArrayIndexKey", diagnostics)?; - self.no_array_index_key = Some(configuration); - } - "noAssignInExpressions" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noAssignInExpressions", - diagnostics, - )?; - self.no_assign_in_expressions = Some(configuration); - } - "noAsyncPromiseExecutor" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noAsyncPromiseExecutor", - diagnostics, - )?; - self.no_async_promise_executor = Some(configuration); - } - "noCatchAssign" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noCatchAssign", diagnostics)?; - self.no_catch_assign = Some(configuration); - } - "noClassAssign" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noClassAssign", diagnostics)?; - self.no_class_assign = Some(configuration); - } - "noCommentText" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noCommentText", diagnostics)?; - self.no_comment_text = Some(configuration); - } - "noCompareNegZero" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noCompareNegZero", diagnostics)?; - self.no_compare_neg_zero = Some(configuration); - } - "noConfusingLabels" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noConfusingLabels", diagnostics)?; - self.no_confusing_labels = Some(configuration); - } - "noConfusingVoidType" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noConfusingVoidType", diagnostics)?; - self.no_confusing_void_type = Some(configuration); - } - "noConsoleLog" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noConsoleLog", diagnostics)?; - self.no_console_log = Some(configuration); - } - "noConstEnum" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noConstEnum", diagnostics)?; - self.no_const_enum = Some(configuration); - } - "noControlCharactersInRegex" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noControlCharactersInRegex", - diagnostics, - )?; - self.no_control_characters_in_regex = Some(configuration); - } - "noDebugger" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noDebugger", diagnostics)?; - self.no_debugger = Some(configuration); - } - "noDoubleEquals" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noDoubleEquals", diagnostics)?; - self.no_double_equals = Some(configuration); - } - "noDuplicateCase" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noDuplicateCase", diagnostics)?; - self.no_duplicate_case = Some(configuration); - } - "noDuplicateClassMembers" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noDuplicateClassMembers", - diagnostics, - )?; - self.no_duplicate_class_members = Some(configuration); - } - "noDuplicateJsxProps" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noDuplicateJsxProps", diagnostics)?; - self.no_duplicate_jsx_props = Some(configuration); - } - "noDuplicateObjectKeys" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noDuplicateObjectKeys", - diagnostics, - )?; - self.no_duplicate_object_keys = Some(configuration); - } - "noDuplicateParameters" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noDuplicateParameters", - diagnostics, - )?; - self.no_duplicate_parameters = Some(configuration); - } - "noEmptyInterface" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noEmptyInterface", diagnostics)?; - self.no_empty_interface = Some(configuration); - } - "noExplicitAny" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noExplicitAny", diagnostics)?; - self.no_explicit_any = Some(configuration); - } - "noExtraNonNullAssertion" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noExtraNonNullAssertion", - diagnostics, - )?; - self.no_extra_non_null_assertion = Some(configuration); - } - "noFallthroughSwitchClause" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noFallthroughSwitchClause", - diagnostics, - )?; - self.no_fallthrough_switch_clause = Some(configuration); - } - "noFunctionAssign" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noFunctionAssign", diagnostics)?; - self.no_function_assign = Some(configuration); - } - "noGlobalIsFinite" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noGlobalIsFinite", diagnostics)?; - self.no_global_is_finite = Some(configuration); - } - "noGlobalIsNan" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noGlobalIsNan", diagnostics)?; - self.no_global_is_nan = Some(configuration); - } - "noImportAssign" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noImportAssign", diagnostics)?; - self.no_import_assign = Some(configuration); - } - "noLabelVar" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noLabelVar", diagnostics)?; - self.no_label_var = Some(configuration); - } - "noPrototypeBuiltins" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noPrototypeBuiltins", diagnostics)?; - self.no_prototype_builtins = Some(configuration); - } - "noRedeclare" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noRedeclare", diagnostics)?; - self.no_redeclare = Some(configuration); - } - "noRedundantUseStrict" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noRedundantUseStrict", - diagnostics, - )?; - self.no_redundant_use_strict = Some(configuration); - } - "noSelfCompare" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noSelfCompare", diagnostics)?; - self.no_self_compare = Some(configuration); - } - "noShadowRestrictedNames" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noShadowRestrictedNames", - diagnostics, - )?; - self.no_shadow_restricted_names = Some(configuration); - } - "noSparseArray" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noSparseArray", diagnostics)?; - self.no_sparse_array = Some(configuration); - } - "noUnsafeDeclarationMerging" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "noUnsafeDeclarationMerging", - diagnostics, - )?; - self.no_unsafe_declaration_merging = Some(configuration); - } - "noUnsafeNegation" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "noUnsafeNegation", diagnostics)?; - self.no_unsafe_negation = Some(configuration); - } - "useDefaultSwitchClauseLast" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration( - &value, - "useDefaultSwitchClauseLast", - diagnostics, - )?; - self.use_default_switch_clause_last = Some(configuration); - } - "useGetterReturn" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useGetterReturn", diagnostics)?; - self.use_getter_return = Some(configuration); - } - "useIsArray" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useIsArray", diagnostics)?; - self.use_is_array = Some(configuration); - } - "useNamespaceKeyword" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useNamespaceKeyword", diagnostics)?; - self.use_namespace_keyword = Some(configuration); - } - "useValidTypeof" => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, "useValidTypeof", diagnostics)?; - self.use_valid_typeof = Some(configuration); - } - _ => { - report_unknown_map_key( - &name, - &[ - "recommended", - "all", - "noArrayIndexKey", - "noAssignInExpressions", - "noAsyncPromiseExecutor", - "noCatchAssign", - "noClassAssign", - "noCommentText", - "noCompareNegZero", - "noConfusingLabels", - "noConfusingVoidType", - "noConsoleLog", - "noConstEnum", - "noControlCharactersInRegex", - "noDebugger", - "noDoubleEquals", - "noDuplicateCase", - "noDuplicateClassMembers", - "noDuplicateJsxProps", - "noDuplicateObjectKeys", - "noDuplicateParameters", - "noEmptyInterface", - "noExplicitAny", - "noExtraNonNullAssertion", - "noFallthroughSwitchClause", - "noFunctionAssign", - "noGlobalIsFinite", - "noGlobalIsNan", - "noImportAssign", - "noLabelVar", - "noPrototypeBuiltins", - "noRedeclare", - "noRedundantUseStrict", - "noSelfCompare", - "noShadowRestrictedNames", - "noSparseArray", - "noUnsafeDeclarationMerging", - "noUnsafeNegation", - "useDefaultSwitchClauseLast", - "useGetterReturn", - "useIsArray", - "useNamespaceKeyword", - "useValidTypeof", - ], - diagnostics, - ); + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output = Suspicious; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut recommended_is_set = false; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "recommended" => { + recommended_is_set = true; + result.recommended = Deserializable::deserialize(value, diagnostics); + } + "all" => { + result.all = Deserializable::deserialize(value, diagnostics); + } + "noArrayIndexKey" => { + result.no_array_index_key = + RuleConfiguration::deserialize_from_rule_name( + "noArrayIndexKey", + value, + diagnostics, + ); + } + "noAssignInExpressions" => { + result.no_assign_in_expressions = + RuleConfiguration::deserialize_from_rule_name( + "noAssignInExpressions", + value, + diagnostics, + ); + } + "noAsyncPromiseExecutor" => { + result.no_async_promise_executor = + RuleConfiguration::deserialize_from_rule_name( + "noAsyncPromiseExecutor", + value, + diagnostics, + ); + } + "noCatchAssign" => { + result.no_catch_assign = RuleConfiguration::deserialize_from_rule_name( + "noCatchAssign", + value, + diagnostics, + ); + } + "noClassAssign" => { + result.no_class_assign = RuleConfiguration::deserialize_from_rule_name( + "noClassAssign", + value, + diagnostics, + ); + } + "noCommentText" => { + result.no_comment_text = RuleConfiguration::deserialize_from_rule_name( + "noCommentText", + value, + diagnostics, + ); + } + "noCompareNegZero" => { + result.no_compare_neg_zero = + RuleConfiguration::deserialize_from_rule_name( + "noCompareNegZero", + value, + diagnostics, + ); + } + "noConfusingLabels" => { + result.no_confusing_labels = + RuleConfiguration::deserialize_from_rule_name( + "noConfusingLabels", + value, + diagnostics, + ); + } + "noConfusingVoidType" => { + result.no_confusing_void_type = + RuleConfiguration::deserialize_from_rule_name( + "noConfusingVoidType", + value, + diagnostics, + ); + } + "noConsoleLog" => { + result.no_console_log = RuleConfiguration::deserialize_from_rule_name( + "noConsoleLog", + value, + diagnostics, + ); + } + "noConstEnum" => { + result.no_const_enum = RuleConfiguration::deserialize_from_rule_name( + "noConstEnum", + value, + diagnostics, + ); + } + "noControlCharactersInRegex" => { + result.no_control_characters_in_regex = + RuleConfiguration::deserialize_from_rule_name( + "noControlCharactersInRegex", + value, + diagnostics, + ); + } + "noDebugger" => { + result.no_debugger = RuleConfiguration::deserialize_from_rule_name( + "noDebugger", + value, + diagnostics, + ); + } + "noDoubleEquals" => { + result.no_double_equals = RuleConfiguration::deserialize_from_rule_name( + "noDoubleEquals", + value, + diagnostics, + ); + } + "noDuplicateCase" => { + result.no_duplicate_case = + RuleConfiguration::deserialize_from_rule_name( + "noDuplicateCase", + value, + diagnostics, + ); + } + "noDuplicateClassMembers" => { + result.no_duplicate_class_members = + RuleConfiguration::deserialize_from_rule_name( + "noDuplicateClassMembers", + value, + diagnostics, + ); + } + "noDuplicateJsxProps" => { + result.no_duplicate_jsx_props = + RuleConfiguration::deserialize_from_rule_name( + "noDuplicateJsxProps", + value, + diagnostics, + ); + } + "noDuplicateObjectKeys" => { + result.no_duplicate_object_keys = + RuleConfiguration::deserialize_from_rule_name( + "noDuplicateObjectKeys", + value, + diagnostics, + ); + } + "noDuplicateParameters" => { + result.no_duplicate_parameters = + RuleConfiguration::deserialize_from_rule_name( + "noDuplicateParameters", + value, + diagnostics, + ); + } + "noEmptyInterface" => { + result.no_empty_interface = + RuleConfiguration::deserialize_from_rule_name( + "noEmptyInterface", + value, + diagnostics, + ); + } + "noExplicitAny" => { + result.no_explicit_any = RuleConfiguration::deserialize_from_rule_name( + "noExplicitAny", + value, + diagnostics, + ); + } + "noExtraNonNullAssertion" => { + result.no_extra_non_null_assertion = + RuleConfiguration::deserialize_from_rule_name( + "noExtraNonNullAssertion", + value, + diagnostics, + ); + } + "noFallthroughSwitchClause" => { + result.no_fallthrough_switch_clause = + RuleConfiguration::deserialize_from_rule_name( + "noFallthroughSwitchClause", + value, + diagnostics, + ); + } + "noFunctionAssign" => { + result.no_function_assign = + RuleConfiguration::deserialize_from_rule_name( + "noFunctionAssign", + value, + diagnostics, + ); + } + "noGlobalIsFinite" => { + result.no_global_is_finite = + RuleConfiguration::deserialize_from_rule_name( + "noGlobalIsFinite", + value, + diagnostics, + ); + } + "noGlobalIsNan" => { + result.no_global_is_nan = RuleConfiguration::deserialize_from_rule_name( + "noGlobalIsNan", + value, + diagnostics, + ); + } + "noImportAssign" => { + result.no_import_assign = RuleConfiguration::deserialize_from_rule_name( + "noImportAssign", + value, + diagnostics, + ); + } + "noLabelVar" => { + result.no_label_var = RuleConfiguration::deserialize_from_rule_name( + "noLabelVar", + value, + diagnostics, + ); + } + "noPrototypeBuiltins" => { + result.no_prototype_builtins = + RuleConfiguration::deserialize_from_rule_name( + "noPrototypeBuiltins", + value, + diagnostics, + ); + } + "noRedeclare" => { + result.no_redeclare = RuleConfiguration::deserialize_from_rule_name( + "noRedeclare", + value, + diagnostics, + ); + } + "noRedundantUseStrict" => { + result.no_redundant_use_strict = + RuleConfiguration::deserialize_from_rule_name( + "noRedundantUseStrict", + value, + diagnostics, + ); + } + "noSelfCompare" => { + result.no_self_compare = RuleConfiguration::deserialize_from_rule_name( + "noSelfCompare", + value, + diagnostics, + ); + } + "noShadowRestrictedNames" => { + result.no_shadow_restricted_names = + RuleConfiguration::deserialize_from_rule_name( + "noShadowRestrictedNames", + value, + diagnostics, + ); + } + "noSparseArray" => { + result.no_sparse_array = RuleConfiguration::deserialize_from_rule_name( + "noSparseArray", + value, + diagnostics, + ); + } + "noUnsafeDeclarationMerging" => { + result.no_unsafe_declaration_merging = + RuleConfiguration::deserialize_from_rule_name( + "noUnsafeDeclarationMerging", + value, + diagnostics, + ); + } + "noUnsafeNegation" => { + result.no_unsafe_negation = + RuleConfiguration::deserialize_from_rule_name( + "noUnsafeNegation", + value, + diagnostics, + ); + } + "useDefaultSwitchClauseLast" => { + result.use_default_switch_clause_last = + RuleConfiguration::deserialize_from_rule_name( + "useDefaultSwitchClauseLast", + value, + diagnostics, + ); + } + "useGetterReturn" => { + result.use_getter_return = + RuleConfiguration::deserialize_from_rule_name( + "useGetterReturn", + value, + diagnostics, + ); + } + "useIsArray" => { + result.use_is_array = RuleConfiguration::deserialize_from_rule_name( + "useIsArray", + value, + diagnostics, + ); + } + "useNamespaceKeyword" => { + result.use_namespace_keyword = + RuleConfiguration::deserialize_from_rule_name( + "useNamespaceKeyword", + value, + diagnostics, + ); + } + "useValidTypeof" => { + result.use_valid_typeof = RuleConfiguration::deserialize_from_rule_name( + "useValidTypeof", + value, + diagnostics, + ); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + &[ + "recommended", + "all", + "noArrayIndexKey", + "noAssignInExpressions", + "noAsyncPromiseExecutor", + "noCatchAssign", + "noClassAssign", + "noCommentText", + "noCompareNegZero", + "noConfusingLabels", + "noConfusingVoidType", + "noConsoleLog", + "noConstEnum", + "noControlCharactersInRegex", + "noDebugger", + "noDoubleEquals", + "noDuplicateCase", + "noDuplicateClassMembers", + "noDuplicateJsxProps", + "noDuplicateObjectKeys", + "noDuplicateParameters", + "noEmptyInterface", + "noExplicitAny", + "noExtraNonNullAssertion", + "noFallthroughSwitchClause", + "noFunctionAssign", + "noGlobalIsFinite", + "noGlobalIsNan", + "noImportAssign", + "noLabelVar", + "noPrototypeBuiltins", + "noRedeclare", + "noRedundantUseStrict", + "noSelfCompare", + "noShadowRestrictedNames", + "noSparseArray", + "noUnsafeDeclarationMerging", + "noUnsafeNegation", + "useDefaultSwitchClauseLast", + "useGetterReturn", + "useIsArray", + "useNamespaceKeyword", + "useValidTypeof", + ], + )); + } + } + } + if recommended_is_set + && matches!(result.recommended, Some(true)) + && matches!(result.all, Some(true)) + { + diagnostics . push (DeserializationDiagnostic :: new (markup ! (< Emphasis > "'recommended'" < / Emphasis > " and " < Emphasis > "'all'" < / Emphasis > " can't be both " < Emphasis > "'true'" < / Emphasis > ". You should choose only one of them.")) . with_range (range) . with_note (markup ! ("Biome will fallback to its defaults for this section."))) ; + return Some(Self::Output::default()); + } + Some(result) } } - Some(()) + value.deserialize(Visitor, diagnostics) } } diff --git a/crates/biome_service/src/configuration/parse/json/vcs.rs b/crates/biome_service/src/configuration/parse/json/vcs.rs index 6964d971f794..e26dd86f0854 100644 --- a/crates/biome_service/src/configuration/parse/json/vcs.rs +++ b/crates/biome_service/src/configuration/parse/json/vcs.rs @@ -1,81 +1,92 @@ use crate::configuration::vcs::{VcsClientKind, VcsConfiguration}; use biome_console::markup; -use biome_deserialize::json::{report_unknown_map_key, report_unknown_variant, VisitJsonNode}; -use biome_deserialize::{DeserializationDiagnostic, VisitNode}; -use biome_json_syntax::{AnyJsonValue, JsonLanguage, JsonStringValue}; -use biome_rowan::{AstNode, SyntaxNode}; +use biome_deserialize::{ + Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, + ExpectedType, +}; +use biome_rowan::TokenText; -impl VcsConfiguration { - const ALLOWED_KEYS: &'static [&'static str] = - &["clientKind", "enabled", "useIgnoreFile", "root"]; +impl Deserializable for VcsConfiguration { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + value.deserialize(VcsConfigurationVisitor, diagnostics) + } } -impl VisitNode for VcsConfiguration { +struct VcsConfigurationVisitor; +impl DeserializationVisitor for VcsConfigurationVisitor { + type Output = VcsConfiguration; + + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + self, + members: impl Iterator, + range: biome_rowan::TextRange, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "clientKind" => { - let mut client_kind = VcsClientKind::default(); - client_kind.map_to_known_string(&value, name_text, diagnostics)?; - self.client_kind = Some(client_kind); - } - "enabled" => { - self.enabled = self.map_to_boolean(&value, name_text, diagnostics); - } - "useIgnoreFile" => { - self.use_ignore_file = self.map_to_boolean(&value, name_text, diagnostics); - } - "root" => { - self.root = self.map_to_string(&value, name_text, diagnostics); - } - _ => { - report_unknown_map_key(&name, Self::ALLOWED_KEYS, diagnostics); + ) -> Option { + const ALLOWED_KEYS: &[&str] = &["clientKind", "enabled", "useIgnoreFile", "root"]; + let mut result = Self::Output::default(); + for (key, value) in members { + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + let key = key.text(); + match key { + "clientKind" => { + result.client_kind = Some(Deserializable::deserialize(value, diagnostics)?); + } + "enabled" => { + result.enabled = Deserializable::deserialize(value, diagnostics); + } + "useIgnoreFile" => { + result.use_ignore_file = Deserializable::deserialize(value, diagnostics); + } + "root" => { + result.root = Deserializable::deserialize(value, diagnostics); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key, + range, + ALLOWED_KEYS, + )); + } } } - Some(()) + if result.client_kind.is_none() && !result.is_disabled() { + diagnostics.push( + DeserializationDiagnostic::new(markup! { + "You enabled the VCS integration, but you didn't specify a client." + }) + .with_range(range) + .with_note("Biome will disable the VCS integration until the issue is fixed."), + ); + result.enabled = Some(false); + } + Some(result) } } -impl VcsClientKind { - const ALLOWED_VARIANTS: &'static [&'static str] = &["git"]; -} - -impl VisitNode for VcsClientKind { - fn visit_value( - &mut self, - node: &SyntaxNode, +impl Deserializable for VcsClientKind { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let node = JsonStringValue::cast_ref(node)?; - if node.inner_string_text().ok()?.text() == "git" { - *self = VcsClientKind::Git; + ) -> Option { + const ALLOWED_VARIANTS: &[&str] = &["git"]; + let range = value.range(); + let value = TokenText::deserialize(value, diagnostics)?; + if let Ok(value) = value.text().parse::() { + Some(value) } else { - report_unknown_variant(&node, Self::ALLOWED_VARIANTS, diagnostics); + diagnostics.push(DeserializationDiagnostic::new_unknown_value( + value.text(), + range, + ALLOWED_VARIANTS, + )); + None } - Some(()) - } -} - -pub(crate) fn validate_vcs_configuration( - node: &AnyJsonValue, - configuration: &mut VcsConfiguration, - diagnostics: &mut Vec, -) { - if configuration.client_kind.is_none() && !configuration.is_disabled() { - diagnostics.push( - DeserializationDiagnostic::new(markup! { - "You enabled the VCS integration, but you didn't specify a client." - }) - .with_range(node.range()) - .with_note("Biome will disable the VCS integration until the issue is fixed."), - ); - configuration.enabled = Some(false); } } diff --git a/crates/biome_service/tests/invalid/files_ignore_incorrect_type.json.snap b/crates/biome_service/tests/invalid/files_ignore_incorrect_type.json.snap index 55532664fe07..59627854398b 100644 --- a/crates/biome_service/tests/invalid/files_ignore_incorrect_type.json.snap +++ b/crates/biome_service/tests/invalid/files_ignore_incorrect_type.json.snap @@ -4,7 +4,7 @@ expression: files_ignore_incorrect_type.json --- files_ignore_incorrect_type.json:3:13 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key ignore is incorrect. Expected a array. + × Incorrect type, expected an array. 1 │ { 2 │ "files": { diff --git a/crates/biome_service/tests/invalid/files_include_incorrect_type.json.snap b/crates/biome_service/tests/invalid/files_include_incorrect_type.json.snap index 6543d1bf0f93..f546623d7474 100644 --- a/crates/biome_service/tests/invalid/files_include_incorrect_type.json.snap +++ b/crates/biome_service/tests/invalid/files_include_incorrect_type.json.snap @@ -4,7 +4,7 @@ expression: files_include_incorrect_type.json --- files_include_incorrect_type.json:3:14 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key include is incorrect. Expected a array. + × Incorrect type, expected an array. 1 │ { 2 │ "files": { diff --git a/crates/biome_service/tests/invalid/files_incorrect_type.json.snap b/crates/biome_service/tests/invalid/files_incorrect_type.json.snap index c7548335949d..4aa7a6b0ff7a 100644 --- a/crates/biome_service/tests/invalid/files_incorrect_type.json.snap +++ b/crates/biome_service/tests/invalid/files_incorrect_type.json.snap @@ -4,7 +4,7 @@ expression: files_incorrect_type.json --- files_incorrect_type.json:2:11 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key files is incorrect. Expected a object. + × Incorrect type, expected an object. 1 │ { > 2 │ "files": "wrong" diff --git a/crates/biome_service/tests/invalid/files_incorrect_type_for_value.json.snap b/crates/biome_service/tests/invalid/files_incorrect_type_for_value.json.snap index 7cf90f54184a..26bf3b0b623f 100644 --- a/crates/biome_service/tests/invalid/files_incorrect_type_for_value.json.snap +++ b/crates/biome_service/tests/invalid/files_incorrect_type_for_value.json.snap @@ -4,7 +4,7 @@ expression: files_incorrect_type_for_value.json --- files_incorrect_type_for_value.json:3:14 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key maxSize is incorrect. Expected a number. + × Incorrect type, expected a number. 1 │ { 2 │ "files": { diff --git a/crates/biome_service/tests/invalid/files_negative_max_size.json.snap b/crates/biome_service/tests/invalid/files_negative_max_size.json.snap index 3f4eec045f43..d8f70702b7ab 100644 --- a/crates/biome_service/tests/invalid/files_negative_max_size.json.snap +++ b/crates/biome_service/tests/invalid/files_negative_max_size.json.snap @@ -4,7 +4,7 @@ expression: files_negative_max_size.json --- files_negative_max_size.json:3:14 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × invalid digit found in string + × The number should be an integer between 1 and 18446744073709551615. 1 │ { 2 │ "files": { @@ -13,7 +13,5 @@ files_negative_max_size.json:3:14 deserialize ━━━━━━━━━━━ 4 │ } 5 │ } - i Value can't be negative - diff --git a/crates/biome_service/tests/invalid/formatter_format_with_errors_incorrect_type.json.snap b/crates/biome_service/tests/invalid/formatter_format_with_errors_incorrect_type.json.snap index e504f17615c6..90c5bb74b8af 100644 --- a/crates/biome_service/tests/invalid/formatter_format_with_errors_incorrect_type.json.snap +++ b/crates/biome_service/tests/invalid/formatter_format_with_errors_incorrect_type.json.snap @@ -4,7 +4,7 @@ expression: formatter_format_with_errors_incorrect_type.json --- formatter_format_with_errors_incorrect_type.json:3:23 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key formatWithErrors is incorrect. Expected a boolean. + × Incorrect type, expected a boolean. 1 │ { 2 │ "formatter": { diff --git a/crates/biome_service/tests/invalid/formatter_incorrect_type.json.snap b/crates/biome_service/tests/invalid/formatter_incorrect_type.json.snap index f04503ec43d3..058ce0f82bf7 100644 --- a/crates/biome_service/tests/invalid/formatter_incorrect_type.json.snap +++ b/crates/biome_service/tests/invalid/formatter_incorrect_type.json.snap @@ -4,7 +4,7 @@ expression: formatter_incorrect_type.json --- formatter_incorrect_type.json:2:15 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key formatter is incorrect. Expected a object. + × Incorrect type, expected an object. 1 │ { > 2 │ "formatter": "string" diff --git a/crates/biome_service/tests/invalid/formatter_line_width_too_high.json.snap b/crates/biome_service/tests/invalid/formatter_line_width_too_high.json.snap index 3f76f9b28a9a..7aaf907149dd 100644 --- a/crates/biome_service/tests/invalid/formatter_line_width_too_high.json.snap +++ b/crates/biome_service/tests/invalid/formatter_line_width_too_high.json.snap @@ -4,7 +4,7 @@ expression: formatter_line_width_too_high.json --- formatter_line_width_too_high.json:3:16 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × number too large to fit in target type + × The number should be an integer between 1 and 320. 1 │ { 2 │ "formatter": { @@ -13,7 +13,5 @@ formatter_line_width_too_high.json:3:16 deserialize ━━━━━━━━━ 4 │ } 5 │ } - i Maximum value accepted is 320 - diff --git a/crates/biome_service/tests/invalid/formatter_line_width_too_higher_than_allowed.json.snap b/crates/biome_service/tests/invalid/formatter_line_width_too_higher_than_allowed.json.snap index 53802c40440b..d44dfffbc258 100644 --- a/crates/biome_service/tests/invalid/formatter_line_width_too_higher_than_allowed.json.snap +++ b/crates/biome_service/tests/invalid/formatter_line_width_too_higher_than_allowed.json.snap @@ -4,8 +4,7 @@ expression: formatter_line_width_too_higher_than_allowed.json --- formatter_line_width_too_higher_than_allowed.json:3:16 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The line width exceeds the maximum value (320) - + × The number should be an integer between 1 and 320. 1 │ { 2 │ "formatter": { @@ -14,7 +13,5 @@ formatter_line_width_too_higher_than_allowed.json:3:16 deserialize ━━━━ 4 │ } 5 │ } - i Maximum value accepted is 320 - diff --git a/crates/biome_service/tests/invalid/organize_imports.json.snap b/crates/biome_service/tests/invalid/organize_imports.json.snap index 80fa9ceae7f2..4a7fe6760193 100644 --- a/crates/biome_service/tests/invalid/organize_imports.json.snap +++ b/crates/biome_service/tests/invalid/organize_imports.json.snap @@ -4,7 +4,7 @@ expression: organize_imports.json --- organize_imports.json:3:14 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key enabled is incorrect. Expected a boolean. + × Incorrect type, expected a boolean. 1 │ { 2 │ "organizeImports": { diff --git a/crates/biome_service/tests/invalid/overrides/incorrect_type.json.snap b/crates/biome_service/tests/invalid/overrides/incorrect_type.json.snap index 1216b409da40..a6dba44c0f8f 100644 --- a/crates/biome_service/tests/invalid/overrides/incorrect_type.json.snap +++ b/crates/biome_service/tests/invalid/overrides/incorrect_type.json.snap @@ -4,7 +4,7 @@ expression: incorrect_type.json --- incorrect_type.json:2:15 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key overrides is incorrect. Expected a array. + × Incorrect type, expected an array. 1 │ { > 2 │ "overrides": {} diff --git a/crates/biome_service/tests/invalid/overrides/incorrect_value_javascript.json.snap b/crates/biome_service/tests/invalid/overrides/incorrect_value_javascript.json.snap index 8db0454a8167..9410ab4e1417 100644 --- a/crates/biome_service/tests/invalid/overrides/incorrect_value_javascript.json.snap +++ b/crates/biome_service/tests/invalid/overrides/incorrect_value_javascript.json.snap @@ -4,7 +4,7 @@ expression: incorrect_value_javascript.json --- incorrect_value_javascript.json:4:18 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key javascript is incorrect. Expected a object. + × Incorrect type, expected an object. 2 │ "overrides": [ 3 │ { diff --git a/crates/biome_service/tests/invalid/schema.json.snap b/crates/biome_service/tests/invalid/schema.json.snap index 4744e0eb6fec..c536fef43dae 100644 --- a/crates/biome_service/tests/invalid/schema.json.snap +++ b/crates/biome_service/tests/invalid/schema.json.snap @@ -4,7 +4,7 @@ expression: schema.json --- schema.json:2:13 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key $schema is incorrect. Expected a string. + × Incorrect type, expected a string. 1 │ { > 2 │ "$schema": false diff --git a/crates/biome_service/tests/invalid/vcs_incorrect_type.json.snap b/crates/biome_service/tests/invalid/vcs_incorrect_type.json.snap index ad21a32878ea..e6ebee8ad80a 100644 --- a/crates/biome_service/tests/invalid/vcs_incorrect_type.json.snap +++ b/crates/biome_service/tests/invalid/vcs_incorrect_type.json.snap @@ -4,7 +4,7 @@ expression: vcs_incorrect_type.json --- vcs_incorrect_type.json:5:20 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key useIgnoreFile is incorrect. Expected a boolean. + × Incorrect type, expected a boolean. 3 │ "enabled": true, 4 │ "clientKind": "git", diff --git a/crates/biome_service/tests/invalid/vcs_missing_client.json.snap b/crates/biome_service/tests/invalid/vcs_missing_client.json.snap index 1910624b9706..73b073ed02d7 100644 --- a/crates/biome_service/tests/invalid/vcs_missing_client.json.snap +++ b/crates/biome_service/tests/invalid/vcs_missing_client.json.snap @@ -4,7 +4,7 @@ expression: vcs_missing_client.json --- vcs_missing_client.json:4:20 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key useIgnoreFile is incorrect. Expected a boolean. + × Incorrect type, expected a boolean. 2 │ "vcs": { 3 │ "enabled": true, diff --git a/crates/biome_service/tests/invalid/wrong_extends_type.json.snap b/crates/biome_service/tests/invalid/wrong_extends_type.json.snap index 5f6fc1d405e6..ba063620c941 100644 --- a/crates/biome_service/tests/invalid/wrong_extends_type.json.snap +++ b/crates/biome_service/tests/invalid/wrong_extends_type.json.snap @@ -4,7 +4,7 @@ expression: wrong_extends_type.json --- wrong_extends_type.json:2:13 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The value of key extends is incorrect. Expected a array. + × Incorrect type, expected an array. 1 │ { > 2 │ "extends": "something" diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index 6c0fbc72195e..e83e377efb22 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -55,7 +55,7 @@ pub fn create_analyzer_options( ); None } else { - let configuration = deserialized.into_deserialized(); + let configuration = deserialized.into_deserialized().unwrap_or_default(); let mut settings = WorkspaceSettings::default(); settings.merge_with_configuration(configuration).unwrap(); let configuration = AnalyzerConfiguration { diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index 84b0a407e17f..e1c422f75078 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -439,7 +439,7 @@ "description": "The maximum complexity score that we allow. Anything higher is considered excessive.", "type": "integer", "format": "uint8", - "minimum": 0.0 + "minimum": 1.0 } }, "additionalProperties": false diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 84b0a407e17f..e1c422f75078 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -439,7 +439,7 @@ "description": "The maximum complexity score that we allow. Anything higher is considered excessive.", "type": "integer", "format": "uint8", - "minimum": 0.0 + "minimum": 1.0 } }, "additionalProperties": false diff --git a/xtask/codegen/src/generate_configuration.rs b/xtask/codegen/src/generate_configuration.rs index fafd3e54c9c6..b21c1bb23a70 100644 --- a/xtask/codegen/src/generate_configuration.rs +++ b/xtask/codegen/src/generate_configuration.rs @@ -151,15 +151,7 @@ pub(crate) fn generate_rules_configuration(mode: Mode) -> Result<()> { rule_visitor_call.push(quote! { #group_name_string_literal => { - let mut visitor = #group_struct_name::default(); - if are_recommended_and_all_correct( - &value, - name_text, - diagnostics, - )? { - visitor.map_to_object(&value, name_text, diagnostics)?; - self.#property_group_name = Some(visitor); - } + result.#property_group_name = Deserializable::deserialize(value, diagnostics); } }); } @@ -312,42 +304,65 @@ pub(crate) fn generate_rules_configuration(mode: Mode) -> Result<()> { let visitors = quote! { use crate::configuration::linter::*; use crate::Rules; - use biome_deserialize::json::{report_unknown_map_key, VisitJsonNode}; - use biome_deserialize::{DeserializationDiagnostic, VisitNode}; - use biome_json_syntax::JsonLanguage; - use biome_rowan::SyntaxNode; - use crate::configuration::parse::json::linter::are_recommended_and_all_correct; - - impl VisitNode for Rules { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, - diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "recommended" => { - self.recommended = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - - "all" => { - self.all = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } + use biome_console::markup; + use biome_deserialize::{Deserializable, DeserializableValue, DeserializationDiagnostic, DeserializationVisitor, ExpectedType}; + use biome_rowan::{TextRange, TokenText}; - #( #rule_visitor_call )* - - _ => { - report_unknown_map_key(&name, &[#( #group_name_list ),*], diagnostics); + impl Deserializable for Rules { + fn deserialize( + value: impl DeserializableValue, + diagnostics: &mut Vec, + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output =Rules; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut recommended_is_set = false; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "recommended" => { + recommended_is_set = true; + result.recommended = Deserializable::deserialize(value, diagnostics); + } + "all" => { + result.all = Deserializable::deserialize(value, diagnostics); + } + #( #rule_visitor_call ),*, + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + &[#( #group_name_list ),*], + )); + } + } + } + if recommended_is_set && matches!(result.recommended, Some(true)) && matches!(result.all, Some(true)) { + diagnostics + .push(DeserializationDiagnostic::new(markup!( + "'recommended'"" and ""'all'"" can't be both ""'true'"". You should choose only one of them." + )) + .with_range(range) + .with_note(markup!("Biome will fallback to its defaults for this section."))); + return Some(Self::Output::default()); + } + Some(result) } } - - Some(()) + value.deserialize(Visitor, diagnostics) } } - #( #visitor_rule_list )* }; @@ -662,40 +677,65 @@ fn generate_visitor(group: &str, rules: &BTreeMap<&'static str, RuleMetadata>) - group_rules.push(Literal::string(rule_name)); visitor_rule_line.push(quote! { #rule_name => { - let mut configuration = RuleConfiguration::default(); - configuration.map_rule_configuration(&value, #rule_name, diagnostics)?; - self.#rule_identifier = Some(configuration); + result.#rule_identifier = RuleConfiguration::deserialize_from_rule_name(#rule_name, value, diagnostics); } }); } quote! { - impl VisitNode for #group_struct_name { - fn visit_map( - &mut self, - key: &SyntaxNode, - value: &SyntaxNode, + impl Deserializable for #group_struct_name { + fn deserialize( + value: impl DeserializableValue, diagnostics: &mut Vec, - ) -> Option<()> { - let (name, value) = self.get_key_and_value(key, value)?; - let name_text = name.inner_string_text().ok()?; - let name_text = name_text.text(); - match name_text { - "recommended" => { - self.recommended = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - - "all" => { - self.all = Some(self.map_to_boolean(&value, name_text, diagnostics)?); - } - - #( #visitor_rule_line ),*, - _ => { - report_unknown_map_key(&name, &[#( #group_rules ),*], diagnostics); + ) -> Option { + struct Visitor; + impl DeserializationVisitor for Visitor { + type Output =#group_struct_name; + const EXPECTED_TYPE: ExpectedType = ExpectedType::MAP; + fn visit_map( + self, + members: impl Iterator, + range: TextRange, + diagnostics: &mut Vec, + ) -> Option { + let mut recommended_is_set = false; + let mut result = Self::Output::default(); + for (key, value) in members { + let key_range = key.range(); + let Some(key) = TokenText::deserialize(key, diagnostics) else { + continue; + }; + match key.text() { + "recommended" => { + recommended_is_set = true; + result.recommended = Deserializable::deserialize(value, diagnostics); + } + "all" => { + result.all = Deserializable::deserialize(value, diagnostics); + } + #( #visitor_rule_line ),*, + _ => { + diagnostics.push(DeserializationDiagnostic::new_unknown_key( + key.text(), + key_range, + &[#( #group_rules ),*], + )); + } + } + } + if recommended_is_set && matches!(result.recommended, Some(true)) && matches!(result.all, Some(true)) { + diagnostics + .push(DeserializationDiagnostic::new(markup!( + "'recommended'"" and ""'all'"" can't be both ""'true'"". You should choose only one of them." + )) + .with_range(range) + .with_note(markup!("Biome will fallback to its defaults for this section."))); + return Some(Self::Output::default()); + } + Some(result) } } - - Some(()) + value.deserialize(Visitor, diagnostics) } } }