diff --git a/.changeset/tender-cycles-draw.md b/.changeset/tender-cycles-draw.md new file mode 100644 index 000000000000..fe0040713d53 --- /dev/null +++ b/.changeset/tender-cycles-draw.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed [#6003](https://github.com/biomejs/biome/issues/6003): `noUselessUndefinedInitialization` no longer reports exported variables initialized to `undefined`. In Svelte 4, this pattern is used to declare optional component props. diff --git a/crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs b/crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs index 36b4e6258f44..20ec7024dec9 100644 --- a/crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs +++ b/crates/biome_js_analyze/src/lint/complexity/no_useless_undefined_initialization.rs @@ -1,15 +1,17 @@ use biome_analyze::{ - Ast, FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, + FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, }; use biome_console::markup; use biome_diagnostics::Severity; use biome_js_factory::make::js_variable_declarator_list; +use biome_js_semantic::CanBeImportedExported; use biome_js_syntax::{JsLanguage, JsSyntaxToken, JsVariableDeclarator, JsVariableStatement}; use biome_rowan::{AstNode, BatchMutationExt, TextRange}; use biome_rowan::{SyntaxTriviaPiece, chain_trivia_pieces}; use biome_rule_options::no_useless_undefined_initialization::NoUselessUndefinedInitializationOptions; use crate::JsRuleAction; +use crate::services::semantic::Semantic; declare_lint_rule! { /// Disallow initializing variables to `undefined`. @@ -49,6 +51,17 @@ declare_lint_rule! { /// } /// ``` /// + /// Exported variables are not flagged because in some frameworks (e.g., Svelte 4), + /// initializing exported variables to `undefined` is used to declare optional props. + /// + /// ```js + /// export let x = undefined; + /// ``` + /// ```js + /// let y = undefined; + /// export { y }; + /// ``` + /// pub NoUselessUndefinedInitialization { version: "1.7.2", name: "noUselessUndefinedInitialization", @@ -61,13 +74,14 @@ declare_lint_rule! { } impl Rule for NoUselessUndefinedInitialization { - type Query = Ast; + type Query = Semantic; type State = (Box, TextRange); type Signals = Box<[Self::State]>; type Options = NoUselessUndefinedInitializationOptions; fn run(ctx: &RuleContext) -> Self::Signals { let statement = ctx.query(); + let model = ctx.model(); let mut signals = vec![]; let Ok(node) = statement.declaration() else { @@ -96,6 +110,19 @@ impl Rule for NoUselessUndefinedInitialization { }; if keyword.is_undefined() { + // Skip if the variable is exported. + // In frameworks like Svelte 4, exported variables with `undefined` + // initialization are used to declare optional props. + let is_exported = decl + .id() + .ok() + .and_then(|id| id.as_any_js_binding()?.as_js_identifier_binding().cloned()) + .is_some_and(|binding| binding.is_exported(model)); + + if is_exported { + continue; + } + let decl_range = initializer.range(); let Some(binding_name) = decl.id().ok().map(|id| id.to_trimmed_text()) else { continue; diff --git a/crates/biome_js_analyze/tests/specs/complexity/noUselessUndefinedInitialization/valid.js b/crates/biome_js_analyze/tests/specs/complexity/noUselessUndefinedInitialization/valid.js index e7960463c25d..c33245307adc 100644 --- a/crates/biome_js_analyze/tests/specs/complexity/noUselessUndefinedInitialization/valid.js +++ b/crates/biome_js_analyze/tests/specs/complexity/noUselessUndefinedInitialization/valid.js @@ -10,3 +10,22 @@ const { bar = undefined } = baz; class Foo { bar = undefined; } + +// Exported variables should not be flagged. +// In frameworks like Svelte 4, exported variables with undefined initialization +// are used to declare optional props. + +// Direct export +export let directExport = undefined; + +// Named export +let namedExport = undefined; +export { namedExport }; + +// Renamed export (Svelte 4 pattern for reserved words like "class") +let className = undefined; +export { className as class }; + +// Export default +let defaultExport = undefined; +export default defaultExport; diff --git a/crates/biome_js_analyze/tests/specs/complexity/noUselessUndefinedInitialization/valid.js.snap b/crates/biome_js_analyze/tests/specs/complexity/noUselessUndefinedInitialization/valid.js.snap index 8e03a737ca86..670f1a217769 100644 --- a/crates/biome_js_analyze/tests/specs/complexity/noUselessUndefinedInitialization/valid.js.snap +++ b/crates/biome_js_analyze/tests/specs/complexity/noUselessUndefinedInitialization/valid.js.snap @@ -1,7 +1,6 @@ --- source: crates/biome_js_analyze/tests/spec_tests.rs expression: valid.js -snapshot_kind: text --- # Input ```js @@ -18,4 +17,23 @@ class Foo { bar = undefined; } +// Exported variables should not be flagged. +// In frameworks like Svelte 4, exported variables with undefined initialization +// are used to declare optional props. + +// Direct export +export let directExport = undefined; + +// Named export +let namedExport = undefined; +export { namedExport }; + +// Renamed export (Svelte 4 pattern for reserved words like "class") +let className = undefined; +export { className as class }; + +// Export default +let defaultExport = undefined; +export default defaultExport; + ```