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
5 changes: 5 additions & 0 deletions .changeset/mighty-shoes-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Improved handling of `defineProps()` macro in Vue components. The [`noVueReservedKeys`](https://biomejs.dev/linter/rules/no-vue-reserved-keys/) rule now avoids false positives in non-setup scripts.
5 changes: 5 additions & 0 deletions crates/biome_html_syntax/src/element_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ impl HtmlElement {
self.is_script_tag() && self.has_attribute("lang", "ts")
}

/// Returns `true` if the element is a `<script setup>` tag.
pub fn is_script_with_setup_attribute(&self) -> bool {
self.is_script_tag() && self.find_attribute_by_name("setup").is_some()
}

/// Returns `true` if the element is a `<script lang="jsx">`
pub fn is_jsx_lang(&self) -> bool {
self.is_script_tag() && self.has_attribute("lang", "jsx")
Expand Down
625 changes: 316 additions & 309 deletions crates/biome_js_analyze/src/frameworks/vue/vue_component.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@ impl Rule for NoVueDuplicateKeys {
type Options = NoVueDuplicateKeysOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let model = ctx.model();
let Some(component) = VueComponent::from_potential_component(
ctx.query(),
ctx.model(),
model,
ctx.source_type(),
ctx.file_path(),
) else {
Expand All @@ -139,6 +140,12 @@ impl Rule for NoVueDuplicateKeys {
// Collect all declarations across all Vue component sections
for declaration in component.declarations(VueDeclarationCollectionFilter::all()) {
if let Some(name) = declaration.declaration_name() {
// Handle cases like `const { foo } = defineProps(...);`.
if let VueDeclaration::Setup(ref setup_decl) = declaration
&& setup_decl.is_assigned_to_props(model)
{
continue;
}
let key = name.text().to_string();
key_declarations.entry(key).or_default().push(declaration);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,11 @@ impl Rule for NoVueReservedKeys {
return Box::new([]);
};
component
.declarations(VueDeclarationCollectionFilter::all())
.declarations(
VueDeclarationCollectionFilter::all()
& !(VueDeclarationCollectionFilter::Setup
| VueDeclarationCollectionFilter::SetupImport),
)
.into_iter()
.filter_map(|declaration| {
if let Some(name) = declaration.declaration_name() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ impl Rule for NoVueSetupPropsReactivityLoss {
AnyPotentialVueComponent::JsCallExpression(call_expr) => {
check_call_expression_setup(call_expr)
}
_ => Self::Signals::default(),
}
}

Expand Down
5 changes: 3 additions & 2 deletions crates/biome_js_analyze/tests/spec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use biome_diagnostics::advice::CodeSuggestionAdvice;
use biome_fs::OsFileSystem;
use biome_js_analyze::JsAnalyzerServices;
use biome_js_parser::{JsParserOptions, parse};
use biome_js_syntax::{AnyJsRoot, EmbeddingKind, JsFileSource, JsLanguage, ModuleKind};
use biome_js_syntax::{AnyJsRoot, JsFileSource, JsLanguage, ModuleKind};
use biome_package::PackageType;
use biome_plugin_loader::AnalyzerGritPlugin;
use biome_rowan::{AstNode, FileSourceError};
Expand Down Expand Up @@ -121,7 +121,8 @@ fn run_test(input: &'static str, _: &str, _: &str, _: &str) {
// This is needed to set the language to TypeScript for Vue files
// because we can't do it in <script> definition in the current implementation.
let source_type = if source_type.as_embedding_kind().is_vue() {
JsFileSource::ts().with_embedding_kind(EmbeddingKind::Vue)
JsFileSource::ts()
.with_embedding_kind(*VueFileHandler::file_source(&input_code).as_embedding_kind())
} else {
source_type
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
; // this is a hack because vue files are still parsed as js/ts files.
interface Props {
$el: string
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
assertion_line: 151
expression: invalid-setup-interface.vue
---
# Input
```ts
; // this is a hack because vue files are still parsed as js/ts files.
interface Props {
$el: string
}
Expand All @@ -14,16 +14,15 @@ defineProps<Props>();

# Diagnostics
```
invalid-setup-interface.vue:3:5 lint/nursery/noVueReservedKeys ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
invalid-setup-interface.vue:2:5 lint/nursery/noVueReservedKeys ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Key $el is reserved in Vue.

1 │ ; // this is a hack because vue files are still parsed as js/ts files.
2 │ interface Props {
> 3 │ $el: string
1 │ interface Props {
> 2 │ $el: string
│ ^^^
4 │ }
5 │ defineProps<Props>();
3 │ }
4 │ defineProps<Props>();

i Rename the key to avoid conflicts with Vue reserved keys.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
; // this is a hack because vue files are still parsed as js/ts files.
type A = {
$el: string
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
assertion_line: 151
expression: invalid-setup-type-alias.vue
---
# Input
```ts
; // this is a hack because vue files are still parsed as js/ts files.
type A = {
$el: string
};
Expand All @@ -14,16 +14,15 @@ defineProps<A>();

# Diagnostics
```
invalid-setup-type-alias.vue:3:5 lint/nursery/noVueReservedKeys ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
invalid-setup-type-alias.vue:2:5 lint/nursery/noVueReservedKeys ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Key $el is reserved in Vue.

1 │ ; // this is a hack because vue files are still parsed as js/ts files.
2 │ type A = {
> 3 │ $el: string
1 │ type A = {
> 2 │ $el: string
│ ^^^
4 │ };
5 │ defineProps<A>();
3 │ };
4 │ defineProps<A>();

i Rename the key to avoid conflicts with Vue reserved keys.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
// defineProps macro doesn't exist in non-setup scripts, so this is valid
defineProps({
$el: String
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
assertion_line: 152
expression: valid-define-props.vue
---
# Input
```ts
// defineProps macro doesn't exist in non-setup scripts, so this is valid
defineProps({
$el: String
});

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup>
// Imports and variable definitions of SFC should not be flagged as reserved keys.
import {$el} from 'module1';
import {default as $slots} from 'module2';
import {x as $watch} from 'module3';
import {$watch as safe} from 'module4';
import $parent from 'module5';
import $destroy, {y as $emit} from 'module6';
const $set = 123;
let $data = 'test';
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
assertion_line: 152
expression: valid-setup-definitions.vue
---
# Input
```ts
// Imports and variable definitions of SFC should not be flagged as reserved keys.
import {$el} from 'module1';
import {default as $slots} from 'module2';
import {x as $watch} from 'module3';
import {$watch as safe} from 'module4';
import $parent from 'module5';
import $destroy, {y as $emit} from 'module6';
const $set = 123;
let $data = 'test';

```
17 changes: 14 additions & 3 deletions crates/biome_js_syntax/src/file_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ pub enum EmbeddingKind {
/// Whether the script is inside Astro frontmatter
frontmatter: bool,
},
Vue,
Vue {
/// Whether the script is inside script tag with setup attribute
setup: bool,
},
Svelte,
#[default]
None,
Expand All @@ -138,7 +141,10 @@ impl EmbeddingKind {
matches!(self, Self::Astro { frontmatter: true })
}
pub const fn is_vue(&self) -> bool {
matches!(self, Self::Vue)
matches!(self, Self::Vue { .. })
}
pub const fn is_vue_setup(&self) -> bool {
matches!(self, Self::Vue { setup: true })
}
pub const fn is_svelte(&self) -> bool {
matches!(self, Self::Svelte)
Expand Down Expand Up @@ -215,7 +221,12 @@ impl JsFileSource {

/// Vue file definition
pub fn vue() -> Self {
Self::js_module().with_embedding_kind(EmbeddingKind::Vue)
Self::js_module().with_embedding_kind(EmbeddingKind::Vue { setup: false })
}

/// Vue file definition with setup attribute
pub fn vue_setup() -> Self {
Self::js_module().with_embedding_kind(EmbeddingKind::Vue { setup: true })
}

/// Svelte file definition
Expand Down
4 changes: 3 additions & 1 deletion crates/biome_service/src/file_handlers/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,9 @@ pub(crate) fn parse_embedded_script(
if html_file_source.is_svelte() {
file_source = file_source.with_embedding_kind(EmbeddingKind::Svelte);
} else if html_file_source.is_vue() {
file_source = file_source.with_embedding_kind(EmbeddingKind::Vue);
file_source = file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: element.is_script_with_setup_attribute(),
});
}
file_source
} else if html_file_source.is_astro() {
Expand Down
Loading
Loading