Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
67 changes: 64 additions & 3 deletions crates/biome_js_analyze/src/frameworks/vue/vue_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnyPotentialVueComponent>;
Expand Down Expand Up @@ -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<Self> {
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<VueComponentName<'a>> {
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
/// <script> export default { props: [ ... ], data: { ... }, ... }; </script>
Expand All @@ -83,7 +131,7 @@ pub enum VueComponent {
Setup(VueSetupComponent),
}

impl VueComponent {
impl AnyVueComponent {
pub fn from_potential_component(
potential_component: &AnyPotentialVueComponent,
model: &SemanticModel,
Expand Down Expand Up @@ -207,7 +255,20 @@ declare_node_union! {
pub AnyVueDataDeclarationsGroup = JsPropertyObjectMember | JsMethodObjectMember
}

impl VueComponentDeclarations for VueComponent {
impl VueComponentDeclarations for VueComponent<'_> {
fn declarations(
&'_ self,
filter: BitFlags<VueDeclarationCollectionFilter>,
) -> Vec<VueDeclaration> {
self.kind.declarations(filter)
}

fn data_declarations_group(&self) -> Option<AnyVueDataDeclarationsGroup> {
self.kind().data_declarations_group()
}
}

impl VueComponentDeclarations for AnyVueComponent {
fn declarations(
&self,
filter: BitFlags<VueDeclarationCollectionFilter>,
Expand Down
Original file line number Diff line number Diff line change
@@ -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()),
// <script setup> components are named by the file name, so we can't infer it here.
Self::Setup(_) => None,
}?;

// Find `name` property
for member in object_expression.members().into_iter().flatten() {
if let AnyJsObjectMember::JsPropertyObjectMember(property) = member {
if property
.name()
.ok()
.and_then(|n| n.name())
.is_none_or(|n| n != "name")
{
continue;
};

if let Ok(value_expr) = property.value() {
let value_expr = value_expr.omit_parentheses();
if let Some(str_lit) = value_expr
.as_any_js_literal_expression()
.and_then(|e| e.as_js_string_literal_expression())
&& let Ok(token_text) = str_lit.inner_string_text()
{
return Some((token_text, str_lit.syntax().text_range()));
}
}
}
}
None
}
}

/// A Vue component name, either extracted from the component definition or inferred from the file path.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum VueComponentName<'a> {
FromComponent((TokenText, TextRange)),
FromPath(&'a str),
}

impl PartialEq<str> for VueComponentName<'_> {
fn eq(&self, other: &str) -> bool {
match self {
VueComponentName::FromComponent((name, _)) => *name == other,
VueComponentName::FromPath(name) => *name == other,
}
}
}

impl PartialOrd<str> for VueComponentName<'_> {
fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
match self {
VueComponentName::FromComponent((name, _)) => name.text().partial_cmp(other),
VueComponentName::FromPath(name) => (*name).partial_cmp(other),
}
}
}

impl Deref for VueComponentName<'_> {
type Target = str;

fn deref(&self) -> &Self::Target {
match self {
VueComponentName::FromComponent((name, _)) => name.text(),
VueComponentName::FromPath(name) => name,
}
}
}

impl AsRef<str> for VueComponentName<'_> {
fn as_ref(&self) -> &str {
match self {
VueComponentName::FromComponent((name, _)) => name.text(),
VueComponentName::FromPath(name) => name,
}
}
}
3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ pub mod use_max_params;
pub mod use_qwik_classlist;
pub mod use_react_function_components;
pub mod use_sorted_classes;
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 ,] } }
pub mod use_vue_multi_word_component_names;
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 ,] } }
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ impl Rule for NoVueDataObjectDeclaration {
ctx.query(),
ctx.model(),
ctx.source_type::<JsFileSource>(),
ctx.file_path(),
)?;

let data_decl = component.data_declarations_group()?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,12 @@ impl Rule for NoVueReservedKeys {
type Options = NoVueReservedKeysOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let Some(component) =
VueComponent::from_potential_component(ctx.query(), ctx.model(), ctx.source_type())
else {
let Some(component) = VueComponent::from_potential_component(
ctx.query(),
ctx.model(),
ctx.source_type(),
ctx.file_path(),
) else {
Comment thread
dyc3 marked this conversation as resolved.
return Box::new([]);
};
component
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ impl Rule for NoVueReservedProps {
ctx.query(),
ctx.model(),
ctx.source_type::<JsFileSource>(),
ctx.file_path(),
) else {
return Box::new([]);
};
Expand Down
Loading
Loading