diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index 4edc064563ce..9545560b2bc2 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs @@ -2697,6 +2697,22 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "vue/multi-word-component-names" => { + if !options.include_inspired { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired); + return false; + } + if !options.include_nursery { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery); + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .unwrap_group_as_mut() + .use_vue_multi_word_component_names + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "vue/no-deprecated-data-object-declaration" => { if !options.include_inspired { results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired); diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index 87b2db94f8e7..d584fa58d940 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -437,6 +437,7 @@ pub enum RuleName { UseValidForDirection, UseValidLang, UseValidTypeof, + UseVueMultiWordComponentNames, UseWhile, UseYield, } @@ -795,6 +796,7 @@ impl RuleName { Self::UseValidForDirection => "useValidForDirection", Self::UseValidLang => "useValidLang", Self::UseValidTypeof => "useValidTypeof", + Self::UseVueMultiWordComponentNames => "useVueMultiWordComponentNames", Self::UseWhile => "useWhile", Self::UseYield => "useYield", } @@ -1149,6 +1151,7 @@ impl RuleName { Self::UseValidForDirection => RuleGroup::Correctness, Self::UseValidLang => RuleGroup::A11y, Self::UseValidTypeof => RuleGroup::Correctness, + Self::UseVueMultiWordComponentNames => RuleGroup::Nursery, Self::UseWhile => RuleGroup::Complexity, Self::UseYield => RuleGroup::Correctness, } @@ -1512,6 +1515,7 @@ impl std::str::FromStr for RuleName { "useValidForDirection" => Ok(Self::UseValidForDirection), "useValidLang" => Ok(Self::UseValidLang), "useValidTypeof" => Ok(Self::UseValidTypeof), + "useVueMultiWordComponentNames" => Ok(Self::UseVueMultiWordComponentNames), "useWhile" => Ok(Self::UseWhile), "useYield" => Ok(Self::UseYield), _ => Err("This rule name doesn't exist."), @@ -4598,7 +4602,7 @@ impl From for Correctness { #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", default, deny_unknown_fields)] #[doc = r" A list of rules that belong to this group"] -pub struct Nursery { # [doc = r" It enables the recommended rules for this group"] # [serde (skip_serializing_if = "Option::is_none")] pub recommended : Option < bool > , # [doc = "Require Promise-like statements to be handled appropriately."] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Prevent import cycles."] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallow Promises to be used in places where they are almost certainly a mistake."] # [serde (skip_serializing_if = "Option::is_none")] pub no_misused_promises : Option < RuleFixConfiguration < biome_rule_options :: no_misused_promises :: NoMisusedPromisesOptions >> , # [doc = "Prevent client components from being async functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_next_async_client_component : Option < RuleConfiguration < biome_rule_options :: no_next_async_client_component :: NoNextAsyncClientComponentOptions >> , # [doc = "Disallow non-null assertions after optional chaining expressions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_non_null_asserted_optional_chain : Option < RuleConfiguration < biome_rule_options :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChainOptions >> , # [doc = "Disallow useVisibleTask$() functions in Qwik components."] # [serde (skip_serializing_if = "Option::is_none")] pub no_qwik_use_visible_task : Option < RuleConfiguration < biome_rule_options :: no_qwik_use_visible_task :: NoQwikUseVisibleTaskOptions >> , # [doc = "Disallow usage of sensitive data such as API keys and tokens."] # [serde (skip_serializing_if = "Option::is_none")] pub no_secrets : Option < RuleConfiguration < biome_rule_options :: no_secrets :: NoSecretsOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope."] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unnecessary_conditions : Option < RuleConfiguration < biome_rule_options :: no_unnecessary_conditions :: NoUnnecessaryConditionsOptions >> , # [doc = "Warn when importing non-existing exports."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Disallow unused catch bindings."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_catch_binding : Option < RuleFixConfiguration < biome_rule_options :: no_useless_catch_binding :: NoUselessCatchBindingOptions >> , # [doc = "Disallow the use of useless undefined."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_undefined : Option < RuleFixConfiguration < biome_rule_options :: no_useless_undefined :: NoUselessUndefinedOptions >> , # [doc = "Enforce that Vue component data options are declared as functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_data_object_declaration : Option < RuleFixConfiguration < biome_rule_options :: no_vue_data_object_declaration :: NoVueDataObjectDeclarationOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_keys :: NoVueReservedKeysOptions >> , # [doc = "Disallow reserved names to be used as props."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Enforces href attribute for \\ elements."] # [serde (skip_serializing_if = "Option::is_none")] pub use_anchor_href : Option < RuleConfiguration < biome_rule_options :: use_anchor_href :: UseAnchorHrefOptions >> , # [doc = "Enforce type definitions to consistently use either interface or type."] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_type_definitions : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_type_definitions :: UseConsistentTypeDefinitionsOptions >> , # [doc = "Require switch-case statements to be exhaustive."] # [serde (skip_serializing_if = "Option::is_none")] pub use_exhaustive_switch_cases : Option < RuleFixConfiguration < biome_rule_options :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCasesOptions >> , # [doc = "Enforce types in functions, methods, variables, and parameters."] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforces that \\ elements have both width and height attributes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_image_size : Option < RuleConfiguration < biome_rule_options :: use_image_size :: UseImageSizeOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions."] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Prefer using the class prop as a classlist over the classnames helper."] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_classlist : Option < RuleConfiguration < biome_rule_options :: use_qwik_classlist :: UseQwikClasslistOptions >> , # [doc = "Enforce that components are defined as functions and never as classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_react_function_components : Option < RuleConfiguration < biome_rule_options :: use_react_function_components :: UseReactFunctionComponentsOptions >> , # [doc = "Enforce the sorting of CSS utility classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> } +pub struct Nursery { # [doc = r" It enables the recommended rules for this group"] # [serde (skip_serializing_if = "Option::is_none")] pub recommended : Option < bool > , # [doc = "Require Promise-like statements to be handled appropriately."] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Prevent import cycles."] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallow Promises to be used in places where they are almost certainly a mistake."] # [serde (skip_serializing_if = "Option::is_none")] pub no_misused_promises : Option < RuleFixConfiguration < biome_rule_options :: no_misused_promises :: NoMisusedPromisesOptions >> , # [doc = "Prevent client components from being async functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_next_async_client_component : Option < RuleConfiguration < biome_rule_options :: no_next_async_client_component :: NoNextAsyncClientComponentOptions >> , # [doc = "Disallow non-null assertions after optional chaining expressions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_non_null_asserted_optional_chain : Option < RuleConfiguration < biome_rule_options :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChainOptions >> , # [doc = "Disallow useVisibleTask$() functions in Qwik components."] # [serde (skip_serializing_if = "Option::is_none")] pub no_qwik_use_visible_task : Option < RuleConfiguration < biome_rule_options :: no_qwik_use_visible_task :: NoQwikUseVisibleTaskOptions >> , # [doc = "Disallow usage of sensitive data such as API keys and tokens."] # [serde (skip_serializing_if = "Option::is_none")] pub no_secrets : Option < RuleConfiguration < biome_rule_options :: no_secrets :: NoSecretsOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope."] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unnecessary_conditions : Option < RuleConfiguration < biome_rule_options :: no_unnecessary_conditions :: NoUnnecessaryConditionsOptions >> , # [doc = "Warn when importing non-existing exports."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Disallow unused catch bindings."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_catch_binding : Option < RuleFixConfiguration < biome_rule_options :: no_useless_catch_binding :: NoUselessCatchBindingOptions >> , # [doc = "Disallow the use of useless undefined."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_undefined : Option < RuleFixConfiguration < biome_rule_options :: no_useless_undefined :: NoUselessUndefinedOptions >> , # [doc = "Enforce that Vue component data options are declared as functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_data_object_declaration : Option < RuleFixConfiguration < biome_rule_options :: no_vue_data_object_declaration :: NoVueDataObjectDeclarationOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_keys :: NoVueReservedKeysOptions >> , # [doc = "Disallow reserved names to be used as props."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Enforces href attribute for \\ elements."] # [serde (skip_serializing_if = "Option::is_none")] pub use_anchor_href : Option < RuleConfiguration < biome_rule_options :: use_anchor_href :: UseAnchorHrefOptions >> , # [doc = "Enforce type definitions to consistently use either interface or type."] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_type_definitions : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_type_definitions :: UseConsistentTypeDefinitionsOptions >> , # [doc = "Require switch-case statements to be exhaustive."] # [serde (skip_serializing_if = "Option::is_none")] pub use_exhaustive_switch_cases : Option < RuleFixConfiguration < biome_rule_options :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCasesOptions >> , # [doc = "Enforce types in functions, methods, variables, and parameters."] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforces that \\ elements have both width and height attributes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_image_size : Option < RuleConfiguration < biome_rule_options :: use_image_size :: UseImageSizeOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions."] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Prefer using the class prop as a classlist over the classnames helper."] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_classlist : Option < RuleConfiguration < biome_rule_options :: use_qwik_classlist :: UseQwikClasslistOptions >> , # [doc = "Enforce that components are defined as functions and never as classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_react_function_components : Option < RuleConfiguration < biome_rule_options :: use_react_function_components :: UseReactFunctionComponentsOptions >> , # [doc = "Enforce the sorting of CSS utility classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> , # [doc = "Enforce multi-word component names in Vue components."] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_multi_word_component_names : Option < RuleConfiguration < biome_rule_options :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNamesOptions >> } impl Nursery { const GROUP_NAME: &'static str = "nursery"; pub(crate) const GROUP_RULES: &'static [&'static str] = &[ @@ -4626,6 +4630,7 @@ impl Nursery { "useQwikClasslist", "useReactFunctionComponents", "useSortedClasses", + "useVueMultiWordComponentNames", ]; const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])]; @@ -4654,6 +4659,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), ]; } impl RuleGroupExt for Nursery { @@ -4785,6 +4791,11 @@ impl RuleGroupExt for Nursery { { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } + if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() + && rule.is_enabled() + { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); + } index_set } fn get_disabled_rules(&self) -> FxHashSet> { @@ -4909,6 +4920,11 @@ impl RuleGroupExt for Nursery { { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } + if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() + && rule.is_disabled() + { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -5035,6 +5051,10 @@ impl RuleGroupExt for Nursery { .use_sorted_classes .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "useVueMultiWordComponentNames" => self + .use_vue_multi_word_component_names + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), _ => None, } } @@ -5067,6 +5087,7 @@ impl From for Nursery { use_qwik_classlist: Some(value.into()), use_react_function_components: Some(value.into()), use_sorted_classes: Some(value.into()), + use_vue_multi_word_component_names: Some(value.into()), } } } diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 051a23f8668e..8d4a56936896 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -195,6 +195,7 @@ define_categories! { "lint/nursery/useQwikClasslist": "https://biomejs.dev/linter/rules/use-qwik-classlist", "lint/nursery/useReactFunctionComponents": "https://biomejs.dev/linter/rules/use-react-function-components", "lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes", + "lint/nursery/useVueMultiWordComponentNames": "https://biomejs.dev/linter/rules/use-vue-multi-word-component-names", "lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread", "lint/performance/noAwaitInLoops": "https://biomejs.dev/linter/rules/no-await-in-loops", "lint/performance/noBarrelFile": "https://biomejs.dev/linter/rules/no-barrel-file", diff --git a/crates/biome_js_analyze/src/frameworks/vue/vue_component.rs b/crates/biome_js_analyze/src/frameworks/vue/vue_component.rs index 9ae38eb7eb6d..ec6eaf65ec5e 100644 --- a/crates/biome_js_analyze/src/frameworks/vue/vue_component.rs +++ b/crates/biome_js_analyze/src/frameworks/vue/vue_component.rs @@ -14,11 +14,16 @@ use biome_js_syntax::{ use biome_rowan::{ AstNode, AstNodeList, AstSeparatedList, TextRange, TokenText, declare_node_union, }; +use camino::Utf8Path; use std::iter; use crate::utils::rename::RenamableNode; use enumflags2::{BitFlags, bitflags}; +mod component_name; + +pub use component_name::VueComponentName; + /// VueComponentQuery is a query type that can be used to find Vue components. /// It can match any potential Vue component. pub type VueComponentQuery = Semantic; @@ -57,9 +62,52 @@ pub enum VueDeclarationCollectionFilter { Computed = 1 << 6, } +pub struct VueComponent<'a> { + kind: AnyVueComponent, + path: &'a Utf8Path, +} + +impl<'a> VueComponent<'a> { + pub fn new(path: &'a Utf8Path, kind: AnyVueComponent) -> Self { + Self { path, kind } + } + + pub fn kind(&self) -> &AnyVueComponent { + &self.kind + } + + pub fn from_potential_component( + potential_component: &AnyPotentialVueComponent, + model: &SemanticModel, + source: &JsFileSource, + path: &'a Utf8Path, + ) -> Option { + let component = + AnyVueComponent::from_potential_component(potential_component, model, source)?; + Some(Self::new(path, component)) + } + + /// The name of the component, if it can be determined. + /// + /// Derived from the file name if the name is not explicitly set in the component definition. + pub fn name(&self) -> Option> { + self.kind() + .component_name() + .map(VueComponentName::FromComponent) + .or_else(|| { + // filename fallback only for Single-File Components + if self.path.extension() == Some("vue") { + self.path.file_stem().map(VueComponentName::FromPath) + } else { + None + } + }) + } +} + /// An abstraction over multiple ways to define a vue component. /// Provides a list of declarations for a component. -pub enum VueComponent { +pub enum AnyVueComponent { /// Options API style Vue component. /// ```html /// @@ -83,7 +131,7 @@ pub enum VueComponent { Setup(VueSetupComponent), } -impl VueComponent { +impl AnyVueComponent { pub fn from_potential_component( potential_component: &AnyPotentialVueComponent, model: &SemanticModel, @@ -207,7 +255,20 @@ declare_node_union! { pub AnyVueDataDeclarationsGroup = JsPropertyObjectMember | JsMethodObjectMember } -impl VueComponentDeclarations for VueComponent { +impl VueComponentDeclarations for VueComponent<'_> { + fn declarations( + &'_ self, + filter: BitFlags, + ) -> Vec { + self.kind.declarations(filter) + } + + fn data_declarations_group(&self) -> Option { + self.kind().data_declarations_group() + } +} + +impl VueComponentDeclarations for AnyVueComponent { fn declarations( &self, filter: BitFlags, diff --git a/crates/biome_js_analyze/src/frameworks/vue/vue_component/component_name.rs b/crates/biome_js_analyze/src/frameworks/vue/vue_component/component_name.rs new file mode 100644 index 000000000000..b3e95ebf6e19 --- /dev/null +++ b/crates/biome_js_analyze/src/frameworks/vue/vue_component/component_name.rs @@ -0,0 +1,98 @@ +use std::ops::Deref; + +use biome_analyze::QueryMatch; + +use super::*; + +impl AnyVueComponent { + /// Try to infer the component's name from its definition. + pub fn component_name(&self) -> Option<(TokenText, TextRange)> { + let object_expression = match self { + Self::OptionsApi(c) => c + .definition_expression() + .and_then(|e| e.inner_expression()) + .and_then(|e| e.as_js_object_expression().cloned()), + Self::CreateApp(c) => c + .definition_expression() + .and_then(|e| e.inner_expression()) + .and_then(|e| e.as_js_object_expression().cloned()), + Self::DefineComponent(c) => c + .definition_expression() + .and_then(|e| e.inner_expression()) + .and_then(|e| e.as_js_object_expression().cloned()), + // + /// ``` + /// + /// ```js,expect_diagnostic + /// import { defineComponent } from "vue"; + /// export default defineComponent({ + /// name: "Header" + /// }); + /// ``` + /// + /// ```js,expect_diagnostic + /// import { createApp } from "vue"; + /// createApp({ + /// name: "Widget" + /// }).mount("#app"); + /// ``` + /// + /// ### Valid + /// + /// ```vue + /// + /// ``` + /// + /// ```js + /// export default { + /// name: "my-component" + /// }; + /// ``` + /// + /// ```js + /// defineComponent({ + /// name: "MyComponent" + /// }); + /// ``` + /// + /// ```js + /// createApp({ name: "MyApp" }).mount("#app"); + /// ``` + /// + /// ## Options + /// + /// ### `ignores` + /// + /// Additional single-word component names to ignore (case-insensitive). The rule already ignores Vue built-in components and `App` by default. + /// + /// ```json,options + /// { + /// "options": { + /// "ignores": [ + /// "Foo" + /// ] + /// } + /// } + /// ``` + /// + /// #### Valid + /// + /// ```vue,use_options + /// + /// ``` + /// + pub UseVueMultiWordComponentNames { + version: "next", + name: "useVueMultiWordComponentNames", + language: "js", + recommended: true, + severity: Severity::Error, + domains: &[RuleDomain::Vue], + sources: &[RuleSource::EslintVueJs("multi-word-component-names").inspired()], + } +} + +impl Rule for UseVueMultiWordComponentNames { + type Query = VueComponentQuery; + type State = Option<(TokenText, TextRange)>; + type Signals = Option; + type Options = UseVueMultiWordComponentNamesOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + // Build potential Vue component; bail if not a component or not in a Vue embedding + let component = VueComponent::from_potential_component( + ctx.query(), + ctx.model(), + ctx.source_type(), + ctx.file_path(), + )?; + let component_name = component.name()?; + if should_report(component_name.as_ref(), ctx.options()) { + if let VueComponentName::FromComponent(name_token) = component_name { + Some(Some(name_token)) + } else { + // Name inferred from path; can't point to precise range, so flag whole component + Some(None) + } + } else { + None + } + } + + fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + // If the state is Some, we can point to the precise range of the name token. Otherwise, we have to flag the whole component. + + let range = state + .as_ref() + .map_or_else(|| ctx.query().range(), |token_text| token_text.1); + let Some(component_name) = state + .as_ref() + .map(|token_text| token_text.0.text()) + .or_else(|| ctx.file_path().file_stem()) + else { + // Can't determine component name; shouldn't happen since we had a name from the component before + // but just in case, avoid crashing and don't report + debug_assert!(false, "should never get here"); + return None; + }; + let got_name_from_file_name = state.is_none(); + + let mut diagnostic = RuleDiagnostic::new( + rule_category!(), + range, + markup! { + "This Component's name ""\""{component_name}"\""" only contains one word." + }, + ).note(markup! { + "Single-word component names can collide with HTML elements and are less descriptive." + }) + .note(markup! { + "Rename the component to have 2 or more words (e.g. \"FooItem\", or \"BarView\")." + }); + + if got_name_from_file_name { + diagnostic = diagnostic.note(markup! { + "The component name was inferred from the file name." + }); + } + Some(diagnostic) + } +} + +/// Built-in single-word names to ignore. +/// +/// "app" is commonly used as the root component name, so we ignore it by default. +/// The others are actual Vue built-in components that are single-word. +const BUILTIN_IGNORES: &[&str] = &[ + "app", + "component", + "slot", + "suspense", + "teleport", + "template", + "transition", +]; + +/// Determines if the given component name should be reported (i.e. is invalid single-word). +fn should_report(name: &str, options: &UseVueMultiWordComponentNamesOptions) -> bool { + if name.is_empty() { + return true; // invalid, not covered by ignores + } + + // We could binary search, but the list is so short that linear scan is probably faster + if BUILTIN_IGNORES.iter().any(|s| s.eq_ignore_ascii_case(name)) { + return false; + } + + for user in &options.ignores { + if name.eq_ignore_ascii_case(user) { + return false; + } + } + + // Report if NOT multi-word + !is_multi_word(name) +} + +/// Multi-word detection without allocating an intermediate string: +/// - Hyphens (`-`) and underscores (`_`) act as explicit separators +/// - Transition from lowercase/digit to uppercase starts a new segment (inserts an implicit hyphen) +/// - Uppercase letter followed by lowercase also starts a new segment (handles cases like "UIButton") +fn is_multi_word(name: &str) -> bool { + let mut segments = 0u8; + let mut chars = name.chars().peekable(); + + while let Some(ch) = chars.next() { + match ch { + '-' | '_' => { + // Explicit separators don't count as segments themselves + + // because there is a separator, we know there is a segment after this + // no need to keep going + return true; + } + _ => { + // Start a new segment + segments += 1; + if segments > 1 { + return true; + } + + // Skip to the end of this segment + let mut prev_was_upper = ch.is_ascii_uppercase(); + while let Some(&next_ch) = chars.peek() { + match next_ch { + '-' | '_' => { + // End of segment due to separator + break; + } + c if c.is_ascii_uppercase() => { + if !prev_was_upper { + // lowercase/digit -> uppercase: start new segment + break; + } + // Check if this uppercase is followed by lowercase (like 'B' in "UIButton") + chars.next(); // consume this uppercase char + if let Some(&after_upper) = chars.peek() + && after_upper.is_ascii_lowercase() + && prev_was_upper + { + // This uppercase starts a new word (UI|Button pattern) + segments += 1; + if segments > 1 { + return true; + } + prev_was_upper = true; + continue; + } + prev_was_upper = true; + } + _ => { + // lowercase or digit - continue in same segment + chars.next(); + prev_was_upper = false; + } + } + } + } + } + if segments > 1 { + return true; + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_multi_word() { + assert!(is_multi_word("MyComponent")); + assert!(is_multi_word("my-component")); + assert!(is_multi_word("myComponent")); + assert!(is_multi_word("MyAppRoot")); + assert!(is_multi_word("MYComponent")); + assert!(is_multi_word("foo_bar")); // underscore counts as separator + assert!(is_multi_word("Foo_BarBaz")); + assert!(is_multi_word("UIButton")); + assert!(is_multi_word("Foo_")); + assert!(is_multi_word("_Foo")); + assert!(is_multi_word("Foo-")); + assert!(is_multi_word("-Foo")); + + assert!(!is_multi_word("App")); + assert!(!is_multi_word("Foo")); + assert!(!is_multi_word("BUTTON")); + } + + #[test] + fn test_should_report_builtin_and_defaults() { + let options = UseVueMultiWordComponentNamesOptions::default(); + // Built-ins / defaults ignored + assert!(!should_report("App", &options)); + assert!(!should_report("app", &options)); + assert!(!should_report("Component", &options)); + assert!(!should_report("component", &options)); + assert!(!should_report("Transition", &options)); + assert!(!should_report("transition-group", &options)); + } + + #[test] + fn test_should_report_user_ignores() { + let mut options = UseVueMultiWordComponentNamesOptions::default(); + options.ignores.push("FooBar".to_string()); // PascalCase + options.ignores.push("widget".to_string()); // lowercase + + // PascalCase ignore and its kebab-case variant + assert!(!should_report("FooBar", &options)); + assert!(!should_report("foo-bar", &options)); + + // Lowercase ignore + assert!(!should_report("widget", &options)); + + // Non-ignored single-word should report + assert!(should_report("Header", &options)); + + // Multi-word never reports + assert!(!should_report("MyWidget", &options)); + } + + #[test] + fn test_should_report_edge_cases() { + let options = UseVueMultiWordComponentNamesOptions::default(); + assert!(should_report("", &options)); // empty invalid + assert!(should_report("X", &options)); // single-letter + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/ValidPascalCase.vue b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/ValidPascalCase.vue new file mode 100644 index 000000000000..1fa6862ff126 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/ValidPascalCase.vue @@ -0,0 +1,5 @@ + diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/ValidPascalCase.vue.snap b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/ValidPascalCase.vue.snap new file mode 100644 index 000000000000..1eef0d017e4e --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/ValidPascalCase.vue.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: ValidPascalCase.vue +--- +# Input +```ts +/* should not generate diagnostics */ +// does not define component name, should grab the name from the file name +export default {} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid-has-name.vue b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid-has-name.vue new file mode 100644 index 000000000000..0217fb8e79ad --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid-has-name.vue @@ -0,0 +1,5 @@ + diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid-has-name.vue.snap b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid-has-name.vue.snap new file mode 100644 index 000000000000..1d2a45112592 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid-has-name.vue.snap @@ -0,0 +1,30 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid-has-name.vue +--- +# Input +```ts +export default { + name: "invalid", +} + +``` + +# Diagnostics +``` +invalid-has-name.vue:2:8 lint/nursery/useVueMultiWordComponentNames ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × This Component's name "invalid" only contains one word. + + 1 │ export default { + > 2 │ name: "invalid", + │ ^^^^^^^^^ + 3 │ } + 4 │ + + i Single-word component names can collide with HTML elements and are less descriptive. + + i Rename the component to have 2 or more words (e.g. "FooItem", or "BarView"). + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.js new file mode 100644 index 000000000000..466dcdf14f2f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.js @@ -0,0 +1,13 @@ +import { defineComponent, createApp } from "vue"; + +/* Invalid: single-word name "Header" (not ignored, not builtin) */ +defineComponent({ + name: "Header" +}); + +/* Invalid: single-word name "Widget" */ +export default { + name: "Widget" +}; + +createApp({ name: "Foo" }) diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.js.snap new file mode 100644 index 000000000000..0c38eb817461 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.js.snap @@ -0,0 +1,59 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```js +import { defineComponent, createApp } from "vue"; + +/* Invalid: single-word name "Header" (not ignored, not builtin) */ +defineComponent({ + name: "Header" +}); + +/* Invalid: single-word name "Widget" */ +export default { + name: "Widget" +}; + +createApp({ name: "Foo" }) + +``` + +# Diagnostics +``` +invalid.js:5:8 lint/nursery/useVueMultiWordComponentNames ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × This Component's name "Header" only contains one word. + + 3 │ /* Invalid: single-word name "Header" (not ignored, not builtin) */ + 4 │ defineComponent({ + > 5 │ name: "Header" + │ ^^^^^^^^ + 6 │ }); + 7 │ + + i Single-word component names can collide with HTML elements and are less descriptive. + + i Rename the component to have 2 or more words (e.g. "FooItem", or "BarView"). + + +``` + +``` +invalid.js:13:19 lint/nursery/useVueMultiWordComponentNames ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × This Component's name "Foo" only contains one word. + + 11 │ }; + 12 │ + > 13 │ createApp({ name: "Foo" }) + │ ^^^^^ + 14 │ + + i Single-word component names can collide with HTML elements and are less descriptive. + + i Rename the component to have 2 or more words (e.g. "FooItem", or "BarView"). + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.vue b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.vue new file mode 100644 index 000000000000..666b4043df4c --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.vue @@ -0,0 +1,4 @@ + diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.vue.snap b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.vue.snap new file mode 100644 index 000000000000..835271be6f40 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/invalid.vue.snap @@ -0,0 +1,30 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.vue +--- +# Input +```ts +// does not define component name, should grab the name from the file name +export default {} + +``` + +# Diagnostics +``` +invalid.vue:2:8 lint/nursery/useVueMultiWordComponentNames ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × This Component's name "invalid" only contains one word. + + 1 │ // does not define component name, should grab the name from the file name + > 2 │ export default {} + │ ^^^^^^^^^^ + 3 │ + + i Single-word component names can collide with HTML elements and are less descriptive. + + i Rename the component to have 2 or more words (e.g. "FooItem", or "BarView"). + + i The component name was inferred from the file name. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid-kebab-case.vue b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid-kebab-case.vue new file mode 100644 index 000000000000..1fa6862ff126 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid-kebab-case.vue @@ -0,0 +1,5 @@ + diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid-kebab-case.vue.snap b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid-kebab-case.vue.snap new file mode 100644 index 000000000000..969981581317 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid-kebab-case.vue.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid-kebab-case.vue +--- +# Input +```ts +/* should not generate diagnostics */ +// does not define component name, should grab the name from the file name +export default {} + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid.js b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid.js new file mode 100644 index 000000000000..372e119ed4a9 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid.js @@ -0,0 +1,24 @@ +/* should not generate diagnostics */ +import { defineComponent, createApp } from "vue"; + +/* Valid: ignored single-word name (default ignore) */ +export default { + name: "App" +}; + +/* Valid: kebab-case multi-word */ +defineComponent({ + name: "my-component" +}); + +/* Valid: PascalCase multi-word */ +defineComponent({ + name: "MyComponent" +}); + +/* Valid: Vue builtin component name (ignored) */ +defineComponent({ + name: "Transition" +}); + +createApp({ name: "MyApp" }) diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid.js.snap new file mode 100644 index 000000000000..2a90e0789551 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/valid.js.snap @@ -0,0 +1,32 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```js +/* should not generate diagnostics */ +import { defineComponent, createApp } from "vue"; + +/* Valid: ignored single-word name (default ignore) */ +export default { + name: "App" +}; + +/* Valid: kebab-case multi-word */ +defineComponent({ + name: "my-component" +}); + +/* Valid: PascalCase multi-word */ +defineComponent({ + name: "MyComponent" +}); + +/* Valid: Vue builtin component name (ignored) */ +defineComponent({ + name: "Transition" +}); + +createApp({ name: "MyApp" }) + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/with-ignore/options.json b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/with-ignore/options.json new file mode 100644 index 000000000000..09adc0f7a048 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/with-ignore/options.json @@ -0,0 +1,18 @@ +{ + "$schema": "../../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "enabled": true, + "rules": { + "nursery": { + "useVueMultiWordComponentNames": { + "level": "error", + "options": { + "ignores": [ + "Foo" + ] + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/with-ignore/valid-ignored.js b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/with-ignore/valid-ignored.js new file mode 100644 index 000000000000..7db02304735a --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/with-ignore/valid-ignored.js @@ -0,0 +1,6 @@ +/* should not generate diagnostics */ + +// shouldn't generate diagnostics because this is in the ignore list +export default { + name: "Foo" +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/with-ignore/valid-ignored.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/with-ignore/valid-ignored.js.snap new file mode 100644 index 000000000000..26a46fb9fe86 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useVueMultiWordComponentNames/with-ignore/valid-ignored.js.snap @@ -0,0 +1,14 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid-ignored.js +--- +# Input +```js +/* should not generate diagnostics */ + +// shouldn't generate diagnostics because this is in the ignore list +export default { + name: "Foo" +} + +``` diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index 71147f85c345..8d39b1c88625 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -354,5 +354,6 @@ pub mod use_valid_autocomplete; pub mod use_valid_for_direction; pub mod use_valid_lang; pub mod use_valid_typeof; +pub mod use_vue_multi_word_component_names; pub mod use_while; pub mod use_yield; diff --git a/crates/biome_rule_options/src/use_vue_multi_word_component_names.rs b/crates/biome_rule_options/src/use_vue_multi_word_component_names.rs new file mode 100644 index 000000000000..82b1371eb28b --- /dev/null +++ b/crates/biome_rule_options/src/use_vue_multi_word_component_names.rs @@ -0,0 +1,10 @@ +use biome_deserialize_macros::Deserializable; +use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, Deserialize, Deserializable, Eq, PartialEq, Serialize, Default)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct UseVueMultiWordComponentNamesOptions { + /// Component names to ignore (allowed to be single-word). + #[serde(skip_serializing_if = "Vec::is_empty")] + pub ignores: Vec, +} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 7cf2946fbd98..f13b65b904c9 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1725,6 +1725,10 @@ export interface Nursery { * Enforce the sorting of CSS utility classes. */ useSortedClasses?: RuleFixConfiguration_for_UseSortedClassesOptions; + /** + * Enforce multi-word component names in Vue components. + */ + useVueMultiWordComponentNames?: RuleConfiguration_for_UseVueMultiWordComponentNamesOptions; } /** * A list of rules that belong to this group @@ -3034,6 +3038,9 @@ export type RuleConfiguration_for_UseReactFunctionComponentsOptions = export type RuleFixConfiguration_for_UseSortedClassesOptions = | RulePlainConfiguration | RuleWithFixOptions_for_UseSortedClassesOptions; +export type RuleConfiguration_for_UseVueMultiWordComponentNamesOptions = + | RulePlainConfiguration + | RuleWithOptions_for_UseVueMultiWordComponentNamesOptions; export type RuleConfiguration_for_NoAccumulatingSpreadOptions = | RulePlainConfiguration | RuleWithOptions_for_NoAccumulatingSpreadOptions; @@ -5591,6 +5598,16 @@ export interface RuleWithFixOptions_for_UseSortedClassesOptions { */ options: UseSortedClassesOptions; } +export interface RuleWithOptions_for_UseVueMultiWordComponentNamesOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: UseVueMultiWordComponentNamesOptions; +} export interface RuleWithOptions_for_NoAccumulatingSpreadOptions { /** * The severity of the emitted diagnostics by the rule @@ -8039,6 +8056,12 @@ export interface UseSortedClassesOptions { */ functions?: string[]; } +export interface UseVueMultiWordComponentNamesOptions { + /** + * Component names to ignore (allowed to be single-word). + */ + ignores: string[]; +} export interface NoAccumulatingSpreadOptions {} export interface NoAwaitInLoopsOptions {} export interface NoBarrelFileOptions {} @@ -8752,6 +8775,7 @@ export type Category = | "lint/nursery/useQwikClasslist" | "lint/nursery/useReactFunctionComponents" | "lint/nursery/useSortedClasses" + | "lint/nursery/useVueMultiWordComponentNames" | "lint/performance/noAccumulatingSpread" | "lint/performance/noAwaitInLoops" | "lint/performance/noBarrelFile" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index acd7e8f008c2..bca486cf7c68 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -5086,6 +5086,15 @@ { "$ref": "#/definitions/UseSortedClassesConfiguration" }, { "type": "null" } ] + }, + "useVueMultiWordComponentNames": { + "description": "Enforce multi-word component names in Vue components.", + "anyOf": [ + { + "$ref": "#/definitions/UseVueMultiWordComponentNamesConfiguration" + }, + { "type": "null" } + ] } }, "additionalProperties": false @@ -11659,6 +11668,23 @@ }, "additionalProperties": false }, + "RuleWithUseVueMultiWordComponentNamesOptions": { + "type": "object", + "required": ["level"], + "properties": { + "level": { + "description": "The severity of the emitted diagnostics by the rule", + "allOf": [{ "$ref": "#/definitions/RulePlainConfiguration" }] + }, + "options": { + "description": "Rule's options", + "allOf": [ + { "$ref": "#/definitions/UseVueMultiWordComponentNamesOptions" } + ] + } + }, + "additionalProperties": false + }, "RuleWithUseWhileOptions": { "type": "object", "required": ["level"], @@ -14504,6 +14530,23 @@ "type": "object", "additionalProperties": false }, + "UseVueMultiWordComponentNamesConfiguration": { + "anyOf": [ + { "$ref": "#/definitions/RulePlainConfiguration" }, + { "$ref": "#/definitions/RuleWithUseVueMultiWordComponentNamesOptions" } + ] + }, + "UseVueMultiWordComponentNamesOptions": { + "type": "object", + "properties": { + "ignores": { + "description": "Component names to ignore (allowed to be single-word).", + "type": "array", + "items": { "type": "string" } + } + }, + "additionalProperties": false + }, "UseWhileConfiguration": { "anyOf": [ { "$ref": "#/definitions/RulePlainConfiguration" },