From cc8cb2f851fc35e31eb11708e3cd54a2a5dfe944 Mon Sep 17 00:00:00 2001 From: Victorien Elvinger Date: Fri, 24 May 2024 14:07:14 +0200 Subject: [PATCH] refcator(migrate/eslint): improve naing-convention migration --- crates/biome_cli/Cargo.toml | 1 + .../src/execute/migrate/eslint_eslint.rs | 2 +- .../src/execute/migrate/eslint_to_biome.rs | 2 +- .../src/execute/migrate/eslint_typescript.rs | 593 ++++++++++++++---- .../tests/commands/migrate_eslint.rs | 20 +- .../migrate_eslintrcjson_rule_options.snap | 242 ++++--- .../src/lint/style/use_naming_convention.rs | 190 ++++-- crates/biome_js_analyze/src/utils/regex.rs | 21 +- .../malformedSelector.js.snap | 12 +- crates/biome_js_syntax/src/modifier_ext.rs | 24 + 10 files changed, 828 insertions(+), 279 deletions(-) diff --git a/crates/biome_cli/Cargo.toml b/crates/biome_cli/Cargo.toml index a6eaa2270074..fe6a623c0b7e 100644 --- a/crates/biome_cli/Cargo.toml +++ b/crates/biome_cli/Cargo.toml @@ -44,6 +44,7 @@ hdrhistogram = { version = "7.5.4", default-features = false } indexmap = { workspace = true } lazy_static = { workspace = true } rayon = { workspace = true } +regex = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/crates/biome_cli/src/execute/migrate/eslint_eslint.rs b/crates/biome_cli/src/execute/migrate/eslint_eslint.rs index 938b8596cf2f..09fd5b4566f1 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_eslint.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_eslint.rs @@ -218,7 +218,7 @@ pub(crate) struct OverrideConfigData { pub(crate) rules: Rules, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Eq, PartialEq)] pub(crate) struct ShorthandVec(Vec); impl Merge for ShorthandVec { fn merge_with(&mut self, mut other: Self) { diff --git a/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs index 83c0eac2b024..01b00db35082 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs @@ -255,7 +255,7 @@ fn migrate_eslint_rule( eslint_eslint::Rule::TypeScriptNamingConvention(conf) => { if migrate_eslint_any_rule(rules, &name, conf.severity(), opts, results) { let severity = conf.severity(); - let options = eslint_typescript::NamingConventionOptions::override_default( + let options = eslint_typescript::NamingConventionOptions::new( conf.into_vec().into_iter().map(|v| *v), ); let group = rules.style.get_or_insert_with(Default::default); diff --git a/crates/biome_cli/src/execute/migrate/eslint_typescript.rs b/crates/biome_cli/src/execute/migrate/eslint_typescript.rs index cda559c01bd1..e3689be9379c 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_typescript.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_typescript.rs @@ -1,6 +1,9 @@ +use std::cmp::Ordering; + /// Configuration related to [TypeScript Eslint](https://typescript-eslint.io/). /// -/// ALso, the module includes implementation to convert rule options to Biome's rule options. +/// Also, the module includes implementation to convert rule options to Biome's rule options. +use biome_deserialize::Deserializable; use biome_deserialize_macros::Deserializable; use biome_js_analyze::lint::style::{use_consistent_array_type, use_naming_convention}; @@ -41,87 +44,103 @@ impl From for use_consistent_array_type::ConsistentArrayType { #[derive(Debug)] pub(crate) struct NamingConventionOptions(Vec); impl NamingConventionOptions { - pub(crate) fn override_default( - overrides: impl IntoIterator, - ) -> Self { - let mut result = Self::default(); - result.0.extend(overrides); - result - } -} -impl Default for NamingConventionOptions { - fn default() -> Self { - Self(vec![ - NamingConventionSelection { - selector: Selector::Default.into(), - format: Some(vec![NamingConventionCase::Camel]), - leading_underscore: Some(Underscore::Allow), - trailing_underscore: Some(Underscore::Allow), - ..Default::default() - }, - NamingConventionSelection { - selector: Selector::Import.into(), - format: Some(vec![ - NamingConventionCase::Camel, - NamingConventionCase::Pascal, - ]), - ..Default::default() - }, - NamingConventionSelection { - selector: Selector::Variable.into(), - format: Some(vec![ - NamingConventionCase::Camel, - NamingConventionCase::Upper, - ]), - leading_underscore: Some(Underscore::Allow), - trailing_underscore: Some(Underscore::Allow), - ..Default::default() - }, - NamingConventionSelection { - selector: Selector::TypeLike.into(), - format: Some(vec![NamingConventionCase::Pascal]), - leading_underscore: Some(Underscore::Allow), - trailing_underscore: Some(Underscore::Allow), - ..Default::default() - }, - ]) + pub(crate) fn new(overrides: impl IntoIterator) -> Self { + let mut inner: Vec<_> = overrides.into_iter().collect(); + // Order of the least general selection to the most geenral selection + inner.sort_by(|a, b| a.precedence(b)); + Self(inner) } } impl From for use_naming_convention::NamingConventionOptions { fn from(val: NamingConventionOptions) -> Self { - let mut enum_member_format = None; + let mut conventions = Vec::new(); for selection in val.0 { - if selection.selector.contains(&Selector::EnumMember) { - // We only extract the first format because Biome doesn't allow for now multiple cases. - enum_member_format = selection - .format - .and_then(|format| format.into_iter().next()); + if selection.types.is_some() || selection.filter.is_some() { + // We don't support types/filter + continue; + } + if selection + .custom + .as_ref() + .is_some_and(|custom| !custom.matches) + { + // We don't support negative matcher + continue; + } + let matching = None; + if selection.custom.is_some() + || selection.leading_underscore.is_some() + || selection.trailing_underscore.is_some() + || !selection.prefix.is_empty() + || !selection.suffix.is_empty() + { + // For now we ignore all fields that require using a regex. + // See the next `FIXME` + continue; + } + // FIXME: Compiling Regexes overflows the stask. + //let mut matching = selection + // .custom + // .as_ref() + // .and_then(|custom| { + // let regex = &custom.regex; + // let regex = match (regex.strip_prefix('^'), regex.strip_suffix('$')) { + // (Some(stripped), Some(_)) => stripped.trim_end_matches('$').to_string(), + // (Some(stripped), None) => format!("(?:{stripped}).*"), + // (None, Some(stripped)) => format!(".*(?:{stripped})"), + // (None, None) => format!(".*(?:{}).*", regex), + // }; + // RestrictedRegex::try_from(regex).ok() + // }); + //let leading_underscore = selection + // .leading_underscore + // .map(|underscore| underscore.as_regex_part()) + // .unwrap_or(""); + //let trailing_underscore = selection + // .trailing_underscore + // .map(|underscore| underscore.as_regex_part()) + // .unwrap_or(""); + //if leading_underscore != "" || trailing_underscore != "" { + // if matching.is_some() { + // // We don't support specifying both `custom` and `leading_underscore`/`trailing_underscore`. + // continue; + // } + // matching = RestrictedRegex::try_from(format!("{leading_underscore}([^_]*){trailing_underscore}")).ok(); + //} + //let prefix = selection.prefix.iter().map(|p| regex::escape(p)).collect::>().join("|"); + //let suffix = selection.prefix.iter().map(|p| regex::escape(p)).collect::>().join("|"); + //if prefix != "" || suffix != "" { + // if matching.is_some() { + // continue; + // } + // matching = RestrictedRegex::try_from(format!("(?:{prefix})(.*)(?:{prefix})")).ok(); + //} + // Ignore the selection if the regex is not valid + //if matching.is_none() && selection.custom.is_some() { + // continue; + //} + let selectors = selection.selectors(); + let formats = if let Some(format) = selection.format { + format + .into_iter() + .map(use_naming_convention::Format::from) + .collect() + } else { + use_naming_convention::Formats::default() + }; + for selector in selectors { + conventions.push(use_naming_convention::Convention { + selector, + matching: matching.clone(), + formats, + }); } } use_naming_convention::NamingConventionOptions { - strict_case: matches!( - enum_member_format, - Some(NamingConventionCase::StrictCamel | NamingConventionCase::StrictPascal) - ), + strict_case: false, require_ascii: false, - conventions: Vec::new(), - enum_member_case: enum_member_format - .and_then(|format| { - match format { - NamingConventionCase::Camel | NamingConventionCase::StrictCamel => { - Some(use_naming_convention::Format::Camel) - } - NamingConventionCase::Pascal | NamingConventionCase::StrictPascal => { - Some(use_naming_convention::Format::Pascal) - } - NamingConventionCase::Upper => { - Some(use_naming_convention::Format::Constant) - } - // Biome doesn't support `snake_case` for enum member - NamingConventionCase::Snake => None, - } - }) - .unwrap_or_default(), + conventions, + enum_member_case: use_naming_convention::Format::default(), } } } @@ -131,39 +150,356 @@ pub(crate) struct NamingConventionSelection { pub(crate) selector: eslint_eslint::ShorthandVec, pub(crate) modifiers: Option>, pub(crate) types: Option>, - //pub(crate) custom: Option, + pub(crate) custom: Option, pub(crate) format: Option>, pub(crate) leading_underscore: Option, pub(crate) trailing_underscore: Option, - //pub(crate) prefix: Option>, - //pub(crate) suffix: Option>, - //pub(crate) filter: Option, -} -//#[derive(Debug, Default, Deserializable)] -//pub(crate) struct Custom { -// regex: String, -// #[deserializable(rename = "match")] -// matches: bool, -//} -//#[derive(Debug, Clone)] -//pub(crate) enum Filter { -// Regex(String), -// Custom(Custom), -//} -//impl Deserializable for Filter { -// fn deserialize( -// value: &impl biome_deserialize::DeserializableValue, -// name: &str, -// diagnostics: &mut Vec, -// ) -> Option { -// if value.visitable()? == VisitableType::STR { -// Deserializable::deserialize(value, name, diagnostics).map(Filter::Regex) -// } else { -// Deserializable::deserialize(value, name, diagnostics).map(Filter::Custom) -// } -// } -//} + pub(crate) prefix: Vec, + pub(crate) suffix: Vec, + pub(crate) filter: Option, +} +impl NamingConventionSelection { + fn precedence(&self, other: &Self) -> Ordering { + // Simplification: We compare only the first selectors. + let selector = self.selector.iter().next(); + let other_selector = other.selector.iter().next(); + match selector.cmp(&other_selector) { + Ordering::Equal => {} + ord => return ord, + } + match (&self.types, &other.types) { + (None, None) | (Some(_), Some(_)) => {} + (None, Some(_)) => return Ordering::Greater, + (Some(_), None) => return Ordering::Less, + } + match (&self.modifiers, &other.modifiers) { + (None, None) | (Some(_), Some(_)) => {} + (None, Some(_)) => return Ordering::Greater, + (Some(_), None) => return Ordering::Less, + } + Ordering::Equal + } + + fn selectors(&self) -> Vec { + let mut result = Vec::new(); + let modifiers: use_naming_convention::Modifiers = self + .modifiers + .iter() + .flatten() + .filter_map(|m| m.as_modifier()) + .collect(); + let has_class_modifier = + modifiers.contains(use_naming_convention::RestrictedModifier::Abstract); + let has_class_member_modifier = modifiers + .contains(use_naming_convention::RestrictedModifier::Private) + || modifiers.contains(use_naming_convention::RestrictedModifier::Protected); + let has_property_modifier = + modifiers.contains(use_naming_convention::RestrictedModifier::Readonly); + modifiers.contains(use_naming_convention::RestrictedModifier::Private); + let scope = self + .modifiers + .iter() + .flatten() + .find_map(|m| m.as_scope()) + .unwrap_or_default(); + for selector in self.selector.iter() { + match selector { + Selector::AutoAccessor => { + // currently unsupported by Biome + continue; + } + Selector::Class => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::Class, + modifiers, + scope, + }); + } + Selector::ClassMethod => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ClassMethod, + modifiers, + scope, + }); + } + Selector::ClassProperty => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ClassProperty, + modifiers, + scope, + }); + } + Selector::Enum => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::Enum, + modifiers, + scope, + }); + } + Selector::EnumMember => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::EnumMember, + modifiers, + scope, + }); + } + Selector::Function => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::Function, + modifiers, + scope, + }); + } + Selector::Import => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ImportNamespace, + modifiers, + scope, + }); + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ImportAlias, + modifiers, + scope, + }); + } + Selector::Interface => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::Interface, + modifiers, + scope, + }); + } + Selector::ObjectLiteralMethod => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ObjectLiteralMethod, + modifiers, + scope, + }); + } + Selector::ObjectLiteralProperty => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ObjectLiteralProperty, + modifiers, + scope, + }); + } + Selector::Parameter => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::FunctionParameter, + modifiers, + scope, + }); + } + Selector::ParameterProperty => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ClassProperty, + modifiers, + scope, + }); + } + Selector::TypeAlias => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::TypeAlias, + modifiers, + scope, + }); + } + Selector::TypeMethod => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::TypeMethod, + modifiers, + scope, + }); + } + Selector::TypeParameter => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::TypeParameter, + modifiers, + scope, + }); + } + Selector::TypeProperty => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::TypeProperty, + modifiers, + scope, + }); + } + Selector::Variable => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::Variable, + modifiers, + scope, + }); + } + Selector::Default => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::Any, + modifiers, + scope, + }); + } + Selector::ClassicAccessor | Selector::Accessor => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ClassGetter, + modifiers, + scope, + }); + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ClassSetter, + modifiers, + scope, + }); + if !has_class_member_modifier { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ObjectLiteralGetter, + modifiers, + scope, + }); + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ObjectLiteralSetter, + modifiers, + scope, + }); + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::TypeGetter, + modifiers, + scope, + }); + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::TypeSetter, + modifiers, + scope, + }); + } + } + Selector::MemberLike => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ClassMember, + modifiers, + scope, + }); + if !has_class_member_modifier { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ObjectLiteralMember, + modifiers, + scope, + }); + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::TypeMember, + modifiers, + scope, + }); + } + } + Selector::Method => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ClassMethod, + modifiers, + scope, + }); + if !has_class_member_modifier { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ObjectLiteralMethod, + modifiers, + scope, + }); + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::TypeMethod, + modifiers, + scope, + }); + } + } + Selector::Property => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ClassProperty, + modifiers, + scope, + }); + if !has_class_member_modifier { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::TypeProperty, + modifiers, + scope, + }); + if !has_property_modifier { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::ObjectLiteralProperty, + modifiers, + scope, + }); + } + } + } + Selector::TypeLike => { + if has_class_modifier { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::Class, + modifiers, + scope, + }); + } else { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::TypeLike, + modifiers, + scope, + }); + } + } + Selector::VariableLike => { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::Variable, + modifiers, + scope, + }); + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::Function, + modifiers, + scope, + }); + if scope != use_naming_convention::Scope::Global { + result.push(use_naming_convention::Selector { + kind: use_naming_convention::Kind::FunctionParameter, + modifiers, + scope, + }); + } + } + } + } + // Remove invalid selectors. + // This avoids to generate errors when loading the Biome configuration. + result.retain(|selector| selector.check().is_ok()); + result + } +} #[derive(Debug, Deserializable)] +pub(crate) struct Custom { + regex: String, + #[deserializable(rename = "match")] + matches: bool, +} +impl Default for Custom { + fn default() -> Self { + Self { + regex: Default::default(), + matches: true, + } + } +} +#[derive(Debug)] +pub(crate) struct Anything; +impl Deserializable for Anything { + fn deserialize( + _value: &impl biome_deserialize::DeserializableValue, + _name: &str, + _diagnostics: &mut Vec, + ) -> Option { + Some(Anything) + } +} +#[derive(Copy, Clone, Debug, Deserializable)] pub(crate) enum NamingConventionCase { #[deserializable(rename = "camelCase")] Camel, @@ -178,8 +514,19 @@ pub(crate) enum NamingConventionCase { #[deserializable(rename = "UPPER_CASE")] Upper, } -#[derive(Debug, Default, Eq, PartialEq, Deserializable)] +impl From for use_naming_convention::Format { + fn from(value: NamingConventionCase) -> Self { + match value { + NamingConventionCase::Camel | NamingConventionCase::StrictCamel => Self::Camel, + NamingConventionCase::Pascal | NamingConventionCase::StrictPascal => Self::Pascal, + NamingConventionCase::Snake => Self::Snake, + NamingConventionCase::Upper => Self::Constant, + } + } +} +#[derive(Debug, Default, Deserializable, Eq, PartialEq, PartialOrd, Ord)] pub(crate) enum Selector { + // Order is important, it reflects the precedence relation between selectors // Individual selectors ClassicAccessor, AutoAccessor, @@ -201,16 +548,16 @@ pub(crate) enum Selector { TypeProperty, Variable, // group selector - #[default] - Default, Accessor, - MemberLike, Method, Property, TypeLike, VariableLike, + MemberLike, + #[default] + Default, } -#[derive(Debug, Deserializable)] +#[derive(Copy, Clone, Debug, Deserializable)] pub(crate) enum Modifier { Abstract, Async, @@ -229,6 +576,24 @@ pub(crate) enum Modifier { Static, Unused, } +impl Modifier { + fn as_modifier(self) -> Option { + match self { + Modifier::Abstract => Some(use_naming_convention::RestrictedModifier::Abstract), + Modifier::Private => Some(use_naming_convention::RestrictedModifier::Private), + Modifier::Protected => Some(use_naming_convention::RestrictedModifier::Protected), + Modifier::Readonly => Some(use_naming_convention::RestrictedModifier::Readonly), + Modifier::Static => Some(use_naming_convention::RestrictedModifier::Static), + _ => None, + } + } + fn as_scope(self) -> Option { + match self { + Modifier::Global => Some(use_naming_convention::Scope::Global), + _ => None, + } + } +} #[derive(Debug, Deserializable)] pub(crate) enum Type { Array, @@ -237,7 +602,7 @@ pub(crate) enum Type { Number, String, } -#[derive(Debug, Deserializable)] +#[derive(Clone, Copy, Debug, Deserializable)] pub(crate) enum Underscore { Forbid, Require, @@ -246,3 +611,15 @@ pub(crate) enum Underscore { AllowDouble, AllowSingleOrDouble, } +//impl Underscore { +// fn as_regex_part(self) -> &'static str { +// match self { +// Self::Forbid => "", +// Self::Require => "_", +// Self::RequireDouble => "__", +// Self::Allow => "_?", +// Self::AllowDouble => "(?:__)?", +// Self::AllowSingleOrDouble => "_?_?", +// } +// } +//} diff --git a/crates/biome_cli/tests/commands/migrate_eslint.rs b/crates/biome_cli/tests/commands/migrate_eslint.rs index 8c3aa0b1b26a..0e06174776e8 100644 --- a/crates/biome_cli/tests/commands/migrate_eslint.rs +++ b/crates/biome_cli/tests/commands/migrate_eslint.rs @@ -255,9 +255,28 @@ fn migrate_eslintrcjson_rule_options() { }], "@typescript-eslint/array-type": ["error", { "default": "generic" }], "@typescript-eslint/naming-convention": ["error", + { + "selector": "property", + "leadingUnderscore": "forbid" + }, + { + "selector": "property", + "modifiers": ["private"], + "format": ["strictCamelCase"], + "leadingUnderscore": "require" + }, + { + "selector": "interface", + "prefix": ["I", "IO"] + }, { "selector": "enumMember", "format": ["UPPER_CASE"] + }, + { + "selector": "variable", + types: ['boolean'], + "format": ["UPPER_CASE"] } ], "unicorn/filename-case": ["error", { @@ -315,7 +334,6 @@ fn migrate_eslintrcjson_rule_options() { Args::from(["migrate", "eslint", "--include-inspired"].as_slice()), ); - assert!(result.is_ok(), "run_cli returned {result:?}"); assert_cli_snapshot(SnapshotPayload::new( module_path!(), "migrate_eslintrcjson_rule_options", diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_rule_options.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_rule_options.snap index 2727db716b41..c7e45fd67915 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_rule_options.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_rule_options.snap @@ -20,9 +20,28 @@ expression: content }], "@typescript-eslint/array-type": ["error", { "default": "generic" }], "@typescript-eslint/naming-convention": ["error", + { + "selector": "property", + "leadingUnderscore": "forbid" + }, + { + "selector": "property", + "modifiers": ["private"], + "format": ["strictCamelCase"], + "leadingUnderscore": "require" + }, + { + "selector": "interface", + "prefix": ["I", "IO"] + }, { "selector": "enumMember", "format": ["UPPER_CASE"] + }, + { + "selector": "variable", + types: ['boolean'], + "format": ["UPPER_CASE"] } ], "unicorn/filename-case": ["error", { @@ -72,101 +91,144 @@ expression: content # Emitted Messages +```block +.eslintrc.json:30:21 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Property key must be double quoted + + 28 │ { + 29 │ "selector": "variable", + > 30 │ types: ['boolean'], + │ ^^^^^ + 31 │ "format": ["UPPER_CASE"] + 32 │ } + + +``` + +```block +.eslintrc.json:30:29 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × JSON standard does not allow single quoted strings + + 28 │ { + 29 │ "selector": "variable", + > 30 │ types: ['boolean'], + │ ^^^^^^^^^ + 31 │ "format": ["UPPER_CASE"] + 32 │ } + + i Use double quotes to escape the string. + + +``` + ```block biome.json migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Configuration file can be updated. - 1 │ - {·"linter":·{·"enabled":·true·}·} - 1 │ + { - 2 │ + → "linter":·{ - 3 │ + → → "enabled":·true, - 4 │ + → → "rules":·{ - 5 │ + → → → "recommended":·false, - 6 │ + → → → "a11y":·{ - 7 │ + → → → → "useValidAriaRole":·{ - 8 │ + → → → → → "level":·"error", - 9 │ + → → → → → "options":·{·"allowInvalidRoles":·["text"],·"ignoreNonDom":·true·} - 10 │ + → → → → } - 11 │ + → → → }, - 12 │ + → → → "style":·{ - 13 │ + → → → → "noRestrictedGlobals":·{ - 14 │ + → → → → → "level":·"error", - 15 │ + → → → → → "options":·{·"deniedGlobals":·["event",·"fdescribe"]·} - 16 │ + → → → → }, - 17 │ + → → → → "useConsistentArrayType":·{ - 18 │ + → → → → → "level":·"error", - 19 │ + → → → → → "options":·{·"syntax":·"generic"·} - 20 │ + → → → → }, - 21 │ + → → → → "useFilenamingConvention":·{ - 22 │ + → → → → → "level":·"error", - 23 │ + → → → → → "options":·{ - 24 │ + → → → → → → "requireAscii":·true, - 25 │ + → → → → → → "filenameCases":·["camelCase",·"PascalCase"] - 26 │ + → → → → → } - 27 │ + → → → → }, - 28 │ + → → → → "useNamingConvention":·{ - 29 │ + → → → → → "level":·"error", - 30 │ + → → → → → "options":·{·"strictCase":·false,·"enumMemberCase":·"CONSTANT_CASE"·} - 31 │ + → → → → } - 32 │ + → → → } - 33 │ + → → } - 34 │ + → }, - 35 │ + → "overrides":·[ - 36 │ + → → { - 37 │ + → → → "include":·["default.js"], - 38 │ + → → → "linter":·{ - 39 │ + → → → → "rules":·{ - 40 │ + → → → → → "a11y":·{·"useValidAriaRole":·"error"·}, - 41 │ + → → → → → "style":·{ - 42 │ + → → → → → → "noRestrictedGlobals":·{·"level":·"error",·"options":·{}·}, - 43 │ + → → → → → → "useConsistentArrayType":·"error", - 44 │ + → → → → → → "useFilenamingConvention":·{ - 45 │ + → → → → → → → "level":·"error", - 46 │ + → → → → → → → "options":·{ - 47 │ + → → → → → → → → "requireAscii":·true, - 48 │ + → → → → → → → → "filenameCases":·["kebab-case"] - 49 │ + → → → → → → → } - 50 │ + → → → → → → }, - 51 │ + → → → → → → "useNamingConvention":·{ - 52 │ + → → → → → → → "level":·"error", - 53 │ + → → → → → → → "options":·{·"strictCase":·false·} - 54 │ + → → → → → → } - 55 │ + → → → → → } - 56 │ + → → → → } - 57 │ + → → → } - 58 │ + → → }, - 59 │ + → → { - 60 │ + → → → "include":·["alternative.js"], - 61 │ + → → → "linter":·{ - 62 │ + → → → → "rules":·{ - 63 │ + → → → → → "style":·{ - 64 │ + → → → → → → "noRestrictedGlobals":·{ - 65 │ + → → → → → → → "level":·"error", - 66 │ + → → → → → → → "options":·{·"deniedGlobals":·["event",·"fdescribe"]·} - 67 │ + → → → → → → }, - 68 │ + → → → → → → "useConsistentArrayType":·{ - 69 │ + → → → → → → → "level":·"error", - 70 │ + → → → → → → → "options":·{·"syntax":·"shorthand"·} - 71 │ + → → → → → → }, - 72 │ + → → → → → → "useFilenamingConvention":·{ - 73 │ + → → → → → → → "level":·"error", - 74 │ + → → → → → → → "options":·{ - 75 │ + → → → → → → → → "requireAscii":·true, - 76 │ + → → → → → → → → "filenameCases":·["kebab-case"] - 77 │ + → → → → → → → } - 78 │ + → → → → → → }, - 79 │ + → → → → → → "useNamingConvention":·{ - 80 │ + → → → → → → → "level":·"error", - 81 │ + → → → → → → → "options":·{·"strictCase":·false·} - 82 │ + → → → → → → } - 83 │ + → → → → → } - 84 │ + → → → → } - 85 │ + → → → } - 86 │ + → → } - 87 │ + → ] - 88 │ + } - 89 │ + + 1 │ - {·"linter":·{·"enabled":·true·}·} + 1 │ + { + 2 │ + → "linter":·{ + 3 │ + → → "enabled":·true, + 4 │ + → → "rules":·{ + 5 │ + → → → "recommended":·false, + 6 │ + → → → "a11y":·{ + 7 │ + → → → → "useValidAriaRole":·{ + 8 │ + → → → → → "level":·"error", + 9 │ + → → → → → "options":·{·"allowInvalidRoles":·["text"],·"ignoreNonDom":·true·} + 10 │ + → → → → } + 11 │ + → → → }, + 12 │ + → → → "style":·{ + 13 │ + → → → → "noRestrictedGlobals":·{ + 14 │ + → → → → → "level":·"error", + 15 │ + → → → → → "options":·{·"deniedGlobals":·["event",·"fdescribe"]·} + 16 │ + → → → → }, + 17 │ + → → → → "useConsistentArrayType":·{ + 18 │ + → → → → → "level":·"error", + 19 │ + → → → → → "options":·{·"syntax":·"generic"·} + 20 │ + → → → → }, + 21 │ + → → → → "useFilenamingConvention":·{ + 22 │ + → → → → → "level":·"error", + 23 │ + → → → → → "options":·{ + 24 │ + → → → → → → "requireAscii":·true, + 25 │ + → → → → → → "filenameCases":·["camelCase",·"PascalCase"] + 26 │ + → → → → → } + 27 │ + → → → → }, + 28 │ + → → → → "useNamingConvention":·{ + 29 │ + → → → → → "level":·"error", + 30 │ + → → → → → "options":·{ + 31 │ + → → → → → → "strictCase":·false, + 32 │ + → → → → → → "conventions":·[ + 33 │ + → → → → → → → { + 34 │ + → → → → → → → → "selector":·{·"kind":·"enumMember"·}, + 35 │ + → → → → → → → → "formats":·["CONSTANT_CASE"] + 36 │ + → → → → → → → } + 37 │ + → → → → → → ] + 38 │ + → → → → → } + 39 │ + → → → → } + 40 │ + → → → } + 41 │ + → → } + 42 │ + → }, + 43 │ + → "overrides":·[ + 44 │ + → → { + 45 │ + → → → "include":·["default.js"], + 46 │ + → → → "linter":·{ + 47 │ + → → → → "rules":·{ + 48 │ + → → → → → "a11y":·{·"useValidAriaRole":·"error"·}, + 49 │ + → → → → → "style":·{ + 50 │ + → → → → → → "noRestrictedGlobals":·{·"level":·"error",·"options":·{}·}, + 51 │ + → → → → → → "useConsistentArrayType":·"error", + 52 │ + → → → → → → "useFilenamingConvention":·{ + 53 │ + → → → → → → → "level":·"error", + 54 │ + → → → → → → → "options":·{ + 55 │ + → → → → → → → → "requireAscii":·true, + 56 │ + → → → → → → → → "filenameCases":·["kebab-case"] + 57 │ + → → → → → → → } + 58 │ + → → → → → → }, + 59 │ + → → → → → → "useNamingConvention":·{ + 60 │ + → → → → → → → "level":·"error", + 61 │ + → → → → → → → "options":·{·"strictCase":·false·} + 62 │ + → → → → → → } + 63 │ + → → → → → } + 64 │ + → → → → } + 65 │ + → → → } + 66 │ + → → }, + 67 │ + → → { + 68 │ + → → → "include":·["alternative.js"], + 69 │ + → → → "linter":·{ + 70 │ + → → → → "rules":·{ + 71 │ + → → → → → "style":·{ + 72 │ + → → → → → → "noRestrictedGlobals":·{ + 73 │ + → → → → → → → "level":·"error", + 74 │ + → → → → → → → "options":·{·"deniedGlobals":·["event",·"fdescribe"]·} + 75 │ + → → → → → → }, + 76 │ + → → → → → → "useConsistentArrayType":·{ + 77 │ + → → → → → → → "level":·"error", + 78 │ + → → → → → → → "options":·{·"syntax":·"shorthand"·} + 79 │ + → → → → → → }, + 80 │ + → → → → → → "useFilenamingConvention":·{ + 81 │ + → → → → → → → "level":·"error", + 82 │ + → → → → → → → "options":·{ + 83 │ + → → → → → → → → "requireAscii":·true, + 84 │ + → → → → → → → → "filenameCases":·["kebab-case"] + 85 │ + → → → → → → → } + 86 │ + → → → → → → }, + 87 │ + → → → → → → "useNamingConvention":·{ + 88 │ + → → → → → → → "level":·"error", + 89 │ + → → → → → → → "options":·{ + 90 │ + → → → → → → → → "strictCase":·false, + 91 │ + → → → → → → → → "conventions":·[{·"formats":·["CONSTANT_CASE"]·}] + 92 │ + → → → → → → → } + 93 │ + → → → → → → } + 94 │ + → → → → → } + 95 │ + → → → → } + 96 │ + → → → } + 97 │ + → → } + 98 │ + → ] + 99 │ + } + 100 │ + ``` diff --git a/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs b/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs index b1a04a8d8b6f..3928625506cb 100644 --- a/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs +++ b/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs @@ -551,7 +551,7 @@ impl Rule for UseNamingConvention { start: name_range_start as u16, end: (name_range_start + name.len()) as u16, }, - suggestion: Suggestion::Formats(convention.formats.clone()), + suggestion: Suggestion::Formats(convention.formats), }); } } @@ -573,7 +573,7 @@ impl Rule for UseNamingConvention { start: name_range_start as u16, end: (name_range_start + name.len()) as u16, }, - suggestion: Suggestion::Formats(default_convention.formats.clone()), + suggestion: Suggestion::Formats(default_convention.formats), }) } @@ -720,7 +720,7 @@ impl AnyIdentifierBindingLike { #[derive(Debug)] pub struct State { - // Selector of the convention which is not fullfilled. + // Selector of the convention which is not fulfilled. convention_selector: Selector, // Range of the name where the suggestion applies name_range: Range, @@ -826,15 +826,15 @@ fn is_default(value: &T) -> bool { pub struct Convention { /// Declarations concerned by this convention #[serde(default, skip_serializing_if = "is_default")] - selector: Selector, + pub selector: Selector, /// Regular expression to enforce #[serde(default, rename = "match", skip_serializing_if = "Option::is_none")] - matching: Option, + pub matching: Option, /// String cases to enforce #[serde(default, skip_serializing_if = "is_default")] - formats: Formats, + pub formats: Formats, } impl DeserializableValidator for Convention { @@ -858,98 +858,126 @@ impl DeserializableValidator for Convention { } } +#[derive(Copy, Clone, Debug)] +pub enum InvalidSelector { + IncompatibleModifiers(Modifier, Modifier), + UnsupportedModifiers(Kind, Modifier), + UnsupportedScope(Kind, Scope), +} +impl std::error::Error for InvalidSelector {} +impl std::fmt::Display for InvalidSelector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InvalidSelector::IncompatibleModifiers(modifier1, modifier2) => { + write!( + f, + "The `{modifier1}` and `{modifier2}` modifiers cannot be used together.", + ) + } + InvalidSelector::UnsupportedModifiers(kind, modifier) => { + write!( + f, + "The `{modifier}` modifier cannot be used with the `{kind}` kind." + ) + } + InvalidSelector::UnsupportedScope(kind, scope) => { + write!( + f, + "The `{scope}` scope cannot be used with the `{kind}` kind." + ) + } + } + } +} + #[derive( Clone, Copy, Debug, Default, Deserializable, Eq, PartialEq, serde::Deserialize, serde::Serialize, )] #[cfg_attr(feature = "schemars", derive(JsonSchema))] #[deserializable(with_validator)] #[serde(deny_unknown_fields)] -struct Selector { +pub struct Selector { /// Declaration kind #[serde(default, skip_serializing_if = "is_default")] - kind: Kind, + pub kind: Kind, /// Modifiers used on the declaration #[serde(default, skip_serializing_if = "is_default")] - modifiers: Modifiers, + pub modifiers: Modifiers, /// Scope of the declaration #[serde(default, skip_serializing_if = "is_default")] - scope: Scope, + pub scope: Scope, } -impl DeserializableValidator for Selector { - fn validate( - &mut self, - _name: &str, - range: biome_rowan::TextRange, - diagnostics: &mut Vec, - ) -> bool { - let accessibility = Modifier::Private | Modifier::Protected; - let class_member_modifiers = accessibility | Modifier::Static; - if self.modifiers.intersects(class_member_modifiers) { - if self.modifiers.0 & accessibility == accessibility { - diagnostics.push( - DeserializationDiagnostic::new( - "The `private` and `protected` modifiers cannot be used toghether.", - ) - .with_range(range), - ); - return false; +impl Selector { + /// Returns an error if the current selector is not valid. + pub fn check(self) -> Result<(), InvalidSelector> { + if self.modifiers.intersects(Modifier::CLASS_MEMBER) { + let accessibility = Modifier::Private | Modifier::Protected; + if *self.modifiers & accessibility == accessibility { + return Err(InvalidSelector::IncompatibleModifiers( + Modifier::Private, + Modifier::Protected, + )); + } + let abstarct_or_static = Modifier::Abstract | Modifier::Static; + if *self.modifiers & abstarct_or_static == abstarct_or_static { + return Err(InvalidSelector::IncompatibleModifiers( + Modifier::Abstract, + Modifier::Static, + )); } if !Kind::ClassMember.contains(self.kind) { - let modifier = self.modifiers.0 & class_member_modifiers; - diagnostics.push( - DeserializationDiagnostic::new(format_args!( - "The `{modifier}` modifier can only be used on class member kinds." - )) - .with_range(range), - ); - return false; + let modifiers = self.modifiers.0 & Modifier::CLASS_MEMBER; + if let Some(modifier) = modifiers.iter().next() { + return Err(InvalidSelector::UnsupportedModifiers(self.kind, modifier)); + } } } if self.modifiers.contains(Modifier::Abstract) { if self.kind != Kind::Class && !Kind::ClassMember.contains(self.kind) { - diagnostics.push( - DeserializationDiagnostic::new( - "The `abstract` modifier can only be used on classes and class member kinds." - ) - .with_range(range), - ); - return false; + return Err(InvalidSelector::UnsupportedModifiers( + self.kind, + Modifier::Abstract, + )); } if self.modifiers.contains(Modifier::Static) { - diagnostics.push( - DeserializationDiagnostic::new( - "The `abstract` and `static` modifiers cannot be used toghether.", - ) - .with_range(range), - ); - return false; + return Err(InvalidSelector::IncompatibleModifiers( + Modifier::Abstract, + Modifier::Static, + )); } } if self.modifiers.contains(Modifier::Readonly) && !matches!(self.kind, Kind::ClassProperty | Kind::TypeProperty) { - diagnostics.push( - DeserializationDiagnostic::new( - "The `readonly` modifier can only be used on class and type property kinds.", - ) - .with_range(range), - ); - return false; + return Err(InvalidSelector::UnsupportedModifiers( + self.kind, + Modifier::Readonly, + )); } if self.scope == Scope::Global && !Kind::Variable.contains(self.kind) && !Kind::Function.contains(self.kind) && !Kind::TypeLike.contains(self.kind) { - diagnostics.push( - DeserializationDiagnostic::new( - "The `global` scope can only be used on type and variable kinds.", - ) - .with_range(range), - ); + return Err(InvalidSelector::UnsupportedScope(self.kind, Scope::Global)); + } + Ok(()) + } +} + +impl DeserializableValidator for Selector { + fn validate( + &mut self, + _name: &str, + range: biome_rowan::TextRange, + diagnostics: &mut Vec, + ) -> bool { + if let Err(error) = self.check() { + diagnostics + .push(DeserializationDiagnostic::new(format_args!("{}", error)).with_range(range)); return false; } true @@ -1071,7 +1099,7 @@ impl Selector { Selector::with_modifiers(Kind::ClassSetter, setter.modifiers()) } }; - // Ignore explicitly overrided members + // Ignore explicitly overridden members (!modifiers.contains(Modifier::Override)).then_some(Selector { kind, modifiers, @@ -1311,7 +1339,7 @@ pub enum Kind { Function, Interface, EnumMember, - /// TypeScript mamespaces, import and export namesapces + /// TypeScript mamespaces, import and export namespaces NamespaceLike, /// TypeScript mamespaces Namespace, @@ -1457,7 +1485,7 @@ impl std::fmt::Display for Kind { #[cfg_attr(feature = "schemars", derive(JsonSchema))] #[serde(rename_all = "camelCase")] #[repr(u16)] -enum RestrictedModifier { +pub enum RestrictedModifier { Abstract = Modifier::Abstract as u16, Private = Modifier::Private as u16, Protected = Modifier::Protected as u16, @@ -1488,6 +1516,11 @@ impl From for RestrictedModifier { } } } +impl From for BitFlags { + fn from(modifier: RestrictedModifier) -> Self { + Modifier::from(modifier).into() + } +} #[derive( Debug, @@ -1505,7 +1538,7 @@ impl From for RestrictedModifier { from = "SmallVec<[RestrictedModifier; 4]>", into = "SmallVec<[RestrictedModifier; 4]>" )] -struct Modifiers(BitFlags); +pub struct Modifiers(BitFlags); impl Deref for Modifiers { type Target = BitFlags; @@ -1525,6 +1558,11 @@ impl From for SmallVec<[RestrictedModifier; 4]> { } impl From> for Modifiers { fn from(values: SmallVec<[RestrictedModifier; 4]>) -> Self { + Self::from_iter(values) + } +} +impl FromIterator for Modifiers { + fn from_iter>(values: T) -> Self { Self( values .into_iter() @@ -1593,7 +1631,7 @@ pub enum Scope { } impl Scope { - /// Returns thes scope of `node` or `None` if the scope cannot be determined or + /// Returns the scope of `node` or `None` if the scope cannot be determined or /// if the scope is an external module. fn from_declaration(node: &AnyJsBindingDeclaration) -> Option { let control_flow_root = node @@ -1684,7 +1722,16 @@ impl TryFrom for Format { } #[derive( - Clone, Debug, Default, Deserializable, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize, + Clone, + Copy, + Debug, + Default, + Deserializable, + Eq, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, )] #[serde(from = "SmallVec<[Format; 4]>", into = "SmallVec<[Format; 4]>")] pub struct Formats(Cases); @@ -1697,6 +1744,11 @@ impl Deref for Formats { } impl From> for Formats { fn from(values: SmallVec<[Format; 4]>) -> Self { + Self::from_iter(values) + } +} +impl FromIterator for Formats { + fn from_iter>(values: T) -> Self { Self(values.into_iter().map(|format| format.into()).collect()) } } diff --git a/crates/biome_js_analyze/src/utils/regex.rs b/crates/biome_js_analyze/src/utils/regex.rs index 9b72849ea21f..bef7c1cf65e9 100644 --- a/crates/biome_js_analyze/src/utils/regex.rs +++ b/crates/biome_js_analyze/src/utils/regex.rs @@ -14,11 +14,23 @@ use biome_deserialize_macros::Deserializable; /// - A limited set of escaped characters including all regex special characters /// and regular string escape characters `\f`, `\n`, `\r`, `\t`, `\v` /// -/// A restricted regular expression is implictly delimited by the anchors `^` and `$`. +/// A restricted regular expression is implicitly delimited by the anchors `^` and `$`. #[derive(Clone, Debug, Deserializable, serde::Deserialize, serde::Serialize)] #[serde(try_from = "String", into = "String")] pub struct RestrictedRegex(regex::Regex); + impl RestrictedRegex { + /// Try to create a restricted regex from a regular regex that allows the anchors `^` and `$`. + pub fn try_from_anchorable(regex: &str) -> Result { + let regex = match (regex.strip_prefix('^'), regex.strip_suffix('$')) { + (Some(stripped), Some(_)) => stripped.trim_end_matches('$').to_string(), + (Some(stripped), None) => format!("(?:{stripped}).*"), + (None, Some(stripped)) => format!(".*(?:{stripped})"), + (None, None) => format!(".*(?:{}).*", regex), + }; + RestrictedRegex::try_from(regex) + } + /// Similar to [regex::Regex::as_str], but returns the original regex representation, /// without the implicit anchors and the implicit group. pub fn as_source(&self) -> &str { @@ -28,6 +40,7 @@ impl RestrictedRegex { &repr[4..(repr.len() - 2)] } } + impl From for String { fn from(value: RestrictedRegex) -> Self { value.into() @@ -75,7 +88,7 @@ impl PartialEq for RestrictedRegex { /// Rteurns an error if `pattern` doesn't follow the restricted regular expression syntax. fn is_restricted_regex(pattern: &str) -> Result<(), regex::Error> { - let mut it = pattern.bytes(); + let mut it = pattern.bytes().peekable(); let mut is_in_char_class = false; while let Some(c) = it.next() { match c { @@ -138,7 +151,8 @@ fn is_restricted_regex(pattern: &str) -> Result<(), regex::Error> { } } b'(' if !is_in_char_class => { - if it.next() == Some(b'?') { + if it.peek() == Some(&b'?') { + it.next(); match it.next() { Some(b'P' | b'=' | b'!' | b'<') => { return if c == b'P' @@ -193,5 +207,6 @@ mod tests { assert!(is_restricted_regex("(?:a)(.+)z").is_ok()); assert!(is_restricted_regex("[A-Z][^a-z]").is_ok()); assert!(is_restricted_regex(r"\n\t\v\f").is_ok()); + assert!(is_restricted_regex("([^_])").is_ok()); } } diff --git a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/malformedSelector.js.snap b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/malformedSelector.js.snap index aa33c65cdd60..2173d44a8210 100644 --- a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/malformedSelector.js.snap +++ b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/malformedSelector.js.snap @@ -26,7 +26,7 @@ expression: malformedSelector.js ``` malformedSelector.options:11:21 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The `private` and `protected` modifiers cannot be used toghether. + × The `private` and `protected` modifiers cannot be used together. 9 │ "conventions": [ 10 │ { @@ -45,7 +45,7 @@ malformedSelector.options:11:21 deserialize ━━━━━━━━━━━━ ``` malformedSelector.options:17:21 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The `abstract` and `static` modifiers cannot be used toghether. + × The `abstract` and `static` modifiers cannot be used together. 15 │ "match": ".*" 16 │ }, { @@ -64,7 +64,7 @@ malformedSelector.options:17:21 deserialize ━━━━━━━━━━━━ ``` malformedSelector.options:23:21 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The `Private` modifier can only be used on class member kinds. + × The `private` modifier cannot be used with the `const` kind. 21 │ "match": ".*" 22 │ }, { @@ -83,7 +83,7 @@ malformedSelector.options:23:21 deserialize ━━━━━━━━━━━━ ``` malformedSelector.options:29:21 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The `readonly` modifier can only be used on class and type property kinds. + × The `readonly` modifier cannot be used with the `const` kind. 27 │ "match": ".*" 28 │ }, { @@ -102,7 +102,7 @@ malformedSelector.options:29:21 deserialize ━━━━━━━━━━━━ ``` malformedSelector.options:35:21 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The `abstract` modifier can only be used on classes and class member kinds. + × The `abstract` modifier cannot be used with the `interface` kind. 33 │ "match": ".*" 34 │ }, { @@ -121,7 +121,7 @@ malformedSelector.options:35:21 deserialize ━━━━━━━━━━━━ ``` malformedSelector.options:41:21 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × The `global` scope can only be used on type and variable kinds. + × The `global ` scope cannot be used with the `class member` kind. 39 │ "match": ".*" 40 │ }, { diff --git a/crates/biome_js_syntax/src/modifier_ext.rs b/crates/biome_js_syntax/src/modifier_ext.rs index 18900860b587..025a8bc74b28 100644 --- a/crates/biome_js_syntax/src/modifier_ext.rs +++ b/crates/biome_js_syntax/src/modifier_ext.rs @@ -1,3 +1,5 @@ +use enumflags2::BitFlags; + use crate::{ AnyJsMethodModifier, AnyJsPropertyModifier, AnyTsIndexSignatureModifier, AnyTsMethodSignatureModifier, AnyTsPropertyParameterModifier, AnyTsPropertySignatureModifier, @@ -24,6 +26,28 @@ pub enum Modifier { Accessor = 1 << 10, } +impl Modifier { + pub const ACCESSIBILITY: BitFlags = BitFlags::::from_bits_truncate_c( + Self::BogusAccessibility as u16 + | Self::Private as u16 + | Self::Protected as u16 + | Self::Public as u16, + BitFlags::CONST_TOKEN, + ); + pub const CLASS_MEMBER: BitFlags = + Self::ACCESSIBILITY.union_c(BitFlags::::from_bits_truncate_c( + Self::Abstract as u16 + | Self::Static as u16 + | Self::Override as u16 + | Self::Accessor as u16, + BitFlags::CONST_TOKEN, + )); + pub const CLASS_TYPE_PROPERTY: BitFlags = BitFlags::::from_bits_truncate_c( + Self::Readonly as u16 | Self::Accessor as u16, + BitFlags::CONST_TOKEN, + ); +} + impl std::fmt::Display for Modifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(