Skip to content

Commit 4416573

Browse files
feat(lint/vue): implement useVueMultiWordComponentNames (#7373)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 099507e commit 4416573

29 files changed

Lines changed: 903 additions & 8 deletions

File tree

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 22 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ define_categories! {
195195
"lint/nursery/useQwikClasslist": "https://biomejs.dev/linter/rules/use-qwik-classlist",
196196
"lint/nursery/useReactFunctionComponents": "https://biomejs.dev/linter/rules/use-react-function-components",
197197
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
198+
"lint/nursery/useVueMultiWordComponentNames": "https://biomejs.dev/linter/rules/use-vue-multi-word-component-names",
198199
"lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread",
199200
"lint/performance/noAwaitInLoops": "https://biomejs.dev/linter/rules/no-await-in-loops",
200201
"lint/performance/noBarrelFile": "https://biomejs.dev/linter/rules/no-barrel-file",

crates/biome_js_analyze/src/frameworks/vue/vue_component.rs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ use biome_js_syntax::{
1414
use biome_rowan::{
1515
AstNode, AstNodeList, AstSeparatedList, TextRange, TokenText, declare_node_union,
1616
};
17+
use camino::Utf8Path;
1718
use std::iter;
1819

1920
use crate::utils::rename::RenamableNode;
2021
use enumflags2::{BitFlags, bitflags};
2122

23+
mod component_name;
24+
25+
pub use component_name::VueComponentName;
26+
2227
/// VueComponentQuery is a query type that can be used to find Vue components.
2328
/// It can match any potential Vue component.
2429
pub type VueComponentQuery = Semantic<AnyPotentialVueComponent>;
@@ -57,9 +62,52 @@ pub enum VueDeclarationCollectionFilter {
5762
Computed = 1 << 6,
5863
}
5964

65+
pub struct VueComponent<'a> {
66+
kind: AnyVueComponent,
67+
path: &'a Utf8Path,
68+
}
69+
70+
impl<'a> VueComponent<'a> {
71+
pub fn new(path: &'a Utf8Path, kind: AnyVueComponent) -> Self {
72+
Self { path, kind }
73+
}
74+
75+
pub fn kind(&self) -> &AnyVueComponent {
76+
&self.kind
77+
}
78+
79+
pub fn from_potential_component(
80+
potential_component: &AnyPotentialVueComponent,
81+
model: &SemanticModel,
82+
source: &JsFileSource,
83+
path: &'a Utf8Path,
84+
) -> Option<Self> {
85+
let component =
86+
AnyVueComponent::from_potential_component(potential_component, model, source)?;
87+
Some(Self::new(path, component))
88+
}
89+
90+
/// The name of the component, if it can be determined.
91+
///
92+
/// Derived from the file name if the name is not explicitly set in the component definition.
93+
pub fn name(&self) -> Option<VueComponentName<'a>> {
94+
self.kind()
95+
.component_name()
96+
.map(VueComponentName::FromComponent)
97+
.or_else(|| {
98+
// filename fallback only for Single-File Components
99+
if self.path.extension() == Some("vue") {
100+
self.path.file_stem().map(VueComponentName::FromPath)
101+
} else {
102+
None
103+
}
104+
})
105+
}
106+
}
107+
60108
/// An abstraction over multiple ways to define a vue component.
61109
/// Provides a list of declarations for a component.
62-
pub enum VueComponent {
110+
pub enum AnyVueComponent {
63111
/// Options API style Vue component.
64112
/// ```html
65113
/// <script> export default { props: [ ... ], data: { ... }, ... }; </script>
@@ -83,7 +131,7 @@ pub enum VueComponent {
83131
Setup(VueSetupComponent),
84132
}
85133

86-
impl VueComponent {
134+
impl AnyVueComponent {
87135
pub fn from_potential_component(
88136
potential_component: &AnyPotentialVueComponent,
89137
model: &SemanticModel,
@@ -207,7 +255,20 @@ declare_node_union! {
207255
pub AnyVueDataDeclarationsGroup = JsPropertyObjectMember | JsMethodObjectMember
208256
}
209257

210-
impl VueComponentDeclarations for VueComponent {
258+
impl VueComponentDeclarations for VueComponent<'_> {
259+
fn declarations(
260+
&'_ self,
261+
filter: BitFlags<VueDeclarationCollectionFilter>,
262+
) -> Vec<VueDeclaration> {
263+
self.kind.declarations(filter)
264+
}
265+
266+
fn data_declarations_group(&self) -> Option<AnyVueDataDeclarationsGroup> {
267+
self.kind().data_declarations_group()
268+
}
269+
}
270+
271+
impl VueComponentDeclarations for AnyVueComponent {
211272
fn declarations(
212273
&self,
213274
filter: BitFlags<VueDeclarationCollectionFilter>,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use std::ops::Deref;
2+
3+
use biome_analyze::QueryMatch;
4+
5+
use super::*;
6+
7+
impl AnyVueComponent {
8+
/// Try to infer the component's name from its definition.
9+
pub fn component_name(&self) -> Option<(TokenText, TextRange)> {
10+
let object_expression = match self {
11+
Self::OptionsApi(c) => c
12+
.definition_expression()
13+
.and_then(|e| e.inner_expression())
14+
.and_then(|e| e.as_js_object_expression().cloned()),
15+
Self::CreateApp(c) => c
16+
.definition_expression()
17+
.and_then(|e| e.inner_expression())
18+
.and_then(|e| e.as_js_object_expression().cloned()),
19+
Self::DefineComponent(c) => c
20+
.definition_expression()
21+
.and_then(|e| e.inner_expression())
22+
.and_then(|e| e.as_js_object_expression().cloned()),
23+
// <script setup> components are named by the file name, so we can't infer it here.
24+
Self::Setup(_) => None,
25+
}?;
26+
27+
// Find `name` property
28+
for member in object_expression.members().into_iter().flatten() {
29+
if let AnyJsObjectMember::JsPropertyObjectMember(property) = member {
30+
if property
31+
.name()
32+
.ok()
33+
.and_then(|n| n.name())
34+
.is_none_or(|n| n != "name")
35+
{
36+
continue;
37+
};
38+
39+
if let Ok(value_expr) = property.value() {
40+
let value_expr = value_expr.omit_parentheses();
41+
if let Some(str_lit) = value_expr
42+
.as_any_js_literal_expression()
43+
.and_then(|e| e.as_js_string_literal_expression())
44+
&& let Ok(token_text) = str_lit.inner_string_text()
45+
{
46+
return Some((token_text, str_lit.syntax().text_range()));
47+
}
48+
}
49+
}
50+
}
51+
None
52+
}
53+
}
54+
55+
/// A Vue component name, either extracted from the component definition or inferred from the file path.
56+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57+
pub enum VueComponentName<'a> {
58+
FromComponent((TokenText, TextRange)),
59+
FromPath(&'a str),
60+
}
61+
62+
impl PartialEq<str> for VueComponentName<'_> {
63+
fn eq(&self, other: &str) -> bool {
64+
match self {
65+
VueComponentName::FromComponent((name, _)) => *name == other,
66+
VueComponentName::FromPath(name) => *name == other,
67+
}
68+
}
69+
}
70+
71+
impl PartialOrd<str> for VueComponentName<'_> {
72+
fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
73+
match self {
74+
VueComponentName::FromComponent((name, _)) => name.text().partial_cmp(other),
75+
VueComponentName::FromPath(name) => (*name).partial_cmp(other),
76+
}
77+
}
78+
}
79+
80+
impl Deref for VueComponentName<'_> {
81+
type Target = str;
82+
83+
fn deref(&self) -> &Self::Target {
84+
match self {
85+
VueComponentName::FromComponent((name, _)) => name.text(),
86+
VueComponentName::FromPath(name) => name,
87+
}
88+
}
89+
}
90+
91+
impl AsRef<str> for VueComponentName<'_> {
92+
fn as_ref(&self) -> &str {
93+
match self {
94+
VueComponentName::FromComponent((name, _)) => name.text(),
95+
VueComponentName::FromPath(name) => name,
96+
}
97+
}
98+
}

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ pub mod use_max_params;
2727
pub mod use_qwik_classlist;
2828
pub mod use_react_function_components;
2929
pub mod use_sorted_classes;
30-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses ,] } }
30+
pub mod use_vue_multi_word_component_names;
31+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }

crates/biome_js_analyze/src/lint/nursery/no_vue_data_object_declaration.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ impl Rule for NoVueDataObjectDeclaration {
124124
ctx.query(),
125125
ctx.model(),
126126
ctx.source_type::<JsFileSource>(),
127+
ctx.file_path(),
127128
)?;
128129

129130
let data_decl = component.data_declarations_group()?;

crates/biome_js_analyze/src/lint/nursery/no_vue_reserved_keys.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,12 @@ impl Rule for NoVueReservedKeys {
116116
type Options = NoVueReservedKeysOptions;
117117

118118
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
119-
let Some(component) =
120-
VueComponent::from_potential_component(ctx.query(), ctx.model(), ctx.source_type())
121-
else {
119+
let Some(component) = VueComponent::from_potential_component(
120+
ctx.query(),
121+
ctx.model(),
122+
ctx.source_type(),
123+
ctx.file_path(),
124+
) else {
122125
return Box::new([]);
123126
};
124127
component

crates/biome_js_analyze/src/lint/nursery/no_vue_reserved_props.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ impl Rule for NoVueReservedProps {
119119
ctx.query(),
120120
ctx.model(),
121121
ctx.source_type::<JsFileSource>(),
122+
ctx.file_path(),
122123
) else {
123124
return Box::new([]);
124125
};

0 commit comments

Comments
 (0)