diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2ea0992b89..dbcd25fac04a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,14 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom The rule reports `else` clauses that can be omitted because their `if` branches break. Contributed by @Conaclos +- Add [noUnusedImports](https://biomejs.dev/linter/rules/no-unused-imports) rule. + The rule reports unused imports and suggests to remove them. + Contributed by @Conaclos + + [noUnusedVariables](https://biomejs.dev/linter/rules/no-unused-variables) reports also unused imports, but don't suggest their removal. + Once [noUnusedImports](https://biomejs.dev/linter/rules/no-unused-imports) stabilized, + [noUnusedVariables](https://biomejs.dev/linter/rules/no-unused-variables) will not report unused imports. + #### Enhancements - The following rules have now safe code fixes: diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index a82756fed962..dfd36adc5ca2 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -97,6 +97,7 @@ define_categories! { "lint/nursery/noGlobalIsNan": "https://biomejs.dev/linter/rules/no-global-is-nan", "lint/nursery/noInvalidNewBuiltin": "https://biomejs.dev/lint/rules/no-invalid-new-builtin", "lint/nursery/noMisleadingInstantiator": "https://biomejs.dev/linter/rules/no-misleading-instantiator", + "lint/nursery/noUnusedImports": "https://biomejs.dev/lint/rules/no-unused-imports", "lint/nursery/noUselessElse": "https://biomejs.dev/lint/rules/no-useless-else", "lint/nursery/noVoid": "https://biomejs.dev/linter/rules/no-void", "lint/nursery/useArrowFunction": "https://biomejs.dev/linter/rules/use-arrow-function", diff --git a/crates/biome_js_analyze/src/react.rs b/crates/biome_js_analyze/src/react.rs index a4f8a7324488..08a55efd1b07 100644 --- a/crates/biome_js_analyze/src/react.rs +++ b/crates/biome_js_analyze/src/react.rs @@ -4,9 +4,9 @@ pub mod hooks; use biome_js_semantic::{Binding, SemanticModel}; use biome_js_syntax::{ - AnyJsCallArgument, AnyJsExpression, AnyJsMemberExpression, AnyJsNamedImportSpecifier, - JsCallExpression, JsIdentifierBinding, JsImport, JsImportNamedClause, - JsNamedImportSpecifierList, JsNamedImportSpecifiers, JsObjectExpression, + AnyJsCallArgument, AnyJsExpression, AnyJsImportClause, AnyJsMemberExpression, AnyJsNamedImport, + AnyJsNamedImportSpecifier, JsCallExpression, JsIdentifierBinding, JsImport, + JsImportNamedClause, JsNamedImportSpecifierList, JsNamedImportSpecifiers, JsObjectExpression, JsPropertyObjectMember, JsxMemberName, JsxReferenceIdentifier, }; use biome_rowan::{AstNode, AstSeparatedList}; @@ -124,14 +124,14 @@ pub(crate) enum ReactLibrary { } impl ReactLibrary { - const fn import_name(self) -> &'static str { + pub const fn import_name(self) -> &'static str { match self { ReactLibrary::React => "react", ReactLibrary::ReactDOM => "react-dom", } } - const fn global_name(self) -> &'static str { + pub const fn global_name(self) -> &'static str { match self { ReactLibrary::React => "React", ReactLibrary::ReactDOM => "ReactDOM", @@ -139,6 +139,53 @@ impl ReactLibrary { } } +/// Return `Some(true)` if `import` imports the global react name from react. +pub(crate) fn is_global_react_import(import: &JsImport, lib: ReactLibrary) -> Option { + let is_react_import = import.source_is(lib.import_name()).ok()?; + if !is_react_import { + return None; + } + let local_name = match import.import_clause().ok()? { + AnyJsImportClause::JsImportBareClause(_) => { + return None; + } + AnyJsImportClause::JsImportDefaultClause(import_clause) => import_clause.local_name(), + AnyJsImportClause::JsImportNamedClause(import_clause) => { + if let Some(default_specifier) = import_clause.default_specifier() { + default_specifier.local_name() + } else { + match import_clause.named_import().ok()? { + AnyJsNamedImport::JsNamedImportSpecifiers(import_clause) => { + import_clause.specifiers().iter().find_map(|specifier| { + let specifier = specifier.ok()?; + let specifier = specifier.as_js_named_import_specifier()?; + specifier + .name() + .ok()? + .is_default() + .ok()? + .then_some(specifier.local_name()) + })? + } + AnyJsNamedImport::JsNamespaceImportSpecifier(import_clause) => { + import_clause.local_name() + } + } + } + } + AnyJsImportClause::JsImportNamespaceClause(import_clause) => import_clause.local_name(), + }; + Some( + local_name + .ok()? + .as_js_identifier_binding()? + .name_token() + .ok()? + .text_trimmed() + == lib.global_name(), + ) +} + /// List of valid [`React` API] /// /// [`React` API]: https://reactjs.org/docs/react-api.html diff --git a/crates/biome_js_analyze/src/semantic_analyzers/correctness/no_unused_variables.rs b/crates/biome_js_analyze/src/semantic_analyzers/correctness/no_unused_variables.rs index 5ad8883b7d15..37a64749ba1a 100644 --- a/crates/biome_js_analyze/src/semantic_analyzers/correctness/no_unused_variables.rs +++ b/crates/biome_js_analyze/src/semantic_analyzers/correctness/no_unused_variables.rs @@ -232,8 +232,8 @@ impl Rule for NoUnusedVariables { let name = binding.name_token().ok()?; let name = name.text_trimmed(); - // Old code import React but do not used directly - // only indirectly after transpiling JSX. + // Legacy React framework requires to import `React`, even if it is not used. + // This is required for transpiling JSX. if name.starts_with('_') || name == "React" { return None; } diff --git a/crates/biome_js_analyze/src/semantic_analyzers/nursery.rs b/crates/biome_js_analyze/src/semantic_analyzers/nursery.rs index 3c3d03e29640..9bf0f79ba173 100644 --- a/crates/biome_js_analyze/src/semantic_analyzers/nursery.rs +++ b/crates/biome_js_analyze/src/semantic_analyzers/nursery.rs @@ -6,6 +6,7 @@ pub(crate) mod no_accumulating_spread; pub(crate) mod no_global_is_finite; pub(crate) mod no_global_is_nan; pub(crate) mod no_invalid_new_builtin; +pub(crate) mod no_unused_imports; pub(crate) mod use_exhaustive_dependencies; pub(crate) mod use_hook_at_top_level; pub(crate) mod use_is_array; @@ -18,6 +19,7 @@ declare_group! { self :: no_global_is_finite :: NoGlobalIsFinite , self :: no_global_is_nan :: NoGlobalIsNan , self :: no_invalid_new_builtin :: NoInvalidNewBuiltin , + self :: no_unused_imports :: NoUnusedImports , self :: use_exhaustive_dependencies :: UseExhaustiveDependencies , self :: use_hook_at_top_level :: UseHookAtTopLevel , self :: use_is_array :: UseIsArray , diff --git a/crates/biome_js_analyze/src/semantic_analyzers/nursery/no_unused_imports.rs b/crates/biome_js_analyze/src/semantic_analyzers/nursery/no_unused_imports.rs new file mode 100644 index 000000000000..2c7846d91004 --- /dev/null +++ b/crates/biome_js_analyze/src/semantic_analyzers/nursery/no_unused_imports.rs @@ -0,0 +1,228 @@ +use crate::{ + react::{is_global_react_import, ReactLibrary}, + semantic_services::Semantic, + JsRuleAction, +}; +use biome_analyze::{context::RuleContext, declare_rule, ActionCategory, Rule, RuleDiagnostic}; +use biome_console::markup; +use biome_diagnostics::Applicability; +use biome_js_factory::make; +use biome_js_semantic::ReferencesExtensions; +use biome_js_syntax::{ + binding_ext::AnyJsBindingDeclaration, AnyJsImportClause, JsFileSource, JsIdentifierBinding, + JsImport, JsImportNamedClause, JsLanguage, JsNamedImportSpecifierList, JsSyntaxNode, T, +}; +use biome_rowan::{ + AstNode, AstSeparatedList, BatchMutation, BatchMutationExt, NodeOrToken, SyntaxResult, +}; + +declare_rule! { + /// Disallow unused imports. + /// + /// Unused imports usually are result of incomplete refactoring. + /// + /// There is one exception to the rule: the `React` import. + /// Importing the `React` variable was a mandatory pattern until some time ago: + /// For the time being this rule will ignore it, + /// but this **might change in the future releases**. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// // Comment + /// import A from 'mod'; + /// ``` + /// + /// ```js,expect_diagnostic + /// // Comment + /// import * as A from 'mod'; + /// ``` + /// + /// ```ts,expect_diagnostic + /// import { type A, B } from 'mod'; + /// + /// export { B } + /// ``` + /// + /// ## Valid + /// + /// ```ts + /// import { A, type B } from 'mod'; + /// + /// function f(arg: B): A { + /// return new A(arg); + /// } + /// ``` + /// + /// ```jsx + /// import React from 'react'; + /// + /// function foo() { + /// return
; + /// }; + /// + /// foo(); + /// ``` + pub(crate) NoUnusedImports { + version: "next", + name: "noUnusedImports", + recommended: false, + } +} + +impl Rule for NoUnusedImports { + type Query = Semantic; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let binding = ctx.query(); + let declaration = binding.declaration()?; + if !is_import(&declaration) { + return None; + } + + if ctx.source_type::().variant().is_jsx() { + let js_import = declaration.syntax().ancestors().find_map(JsImport::cast)?; + // Legacy React framework requires to import `React`, even if it is not used. + // This is required for transpiling JSX. + if is_global_react_import(&js_import, ReactLibrary::React).unwrap_or(false) { + return None; + } + } + + let model = ctx.model(); + binding.all_references(model).next().is_none().then_some(()) + } + + fn diagnostic(ctx: &RuleContext, _: &Self::State) -> Option { + let binding = ctx.query(); + Some( + RuleDiagnostic::new( + rule_category!(), + binding.range(), + markup! { + "This ""import"" is unused." + }, + ) + .note(markup! { + "Unused imports usually are result of incomplete refactoring." + }), + ) + } + + fn action(ctx: &RuleContext, _: &Self::State) -> Option { + let declaration = ctx.query().declaration()?; + let mut mutation = ctx.root().begin(); + match declaration { + AnyJsBindingDeclaration::JsImportDefaultClause(_) + | AnyJsBindingDeclaration::JsImportNamespaceClause(_) => { + let import = declaration.parent::()?; + transfer_leading_trivia_to_sibling(&mut mutation, import.syntax()); + mutation.remove_node(import); + } + AnyJsBindingDeclaration::JsShorthandNamedImportSpecifier(_) + | AnyJsBindingDeclaration::JsNamedImportSpecifier(_) + | AnyJsBindingDeclaration::JsBogusNamedImportSpecifier(_) => { + let specifier_list = declaration.parent::()?; + if specifier_list.iter().count() == 1 { + let import_clause = + JsImportNamedClause::cast(specifier_list.syntax().parent()?.parent()?)?; + remove_named_import_from_import_clause(&mut mutation, import_clause).ok()?; + } else { + let following_separator = specifier_list + .iter() + .zip(specifier_list.separators().map(|separator| separator.ok())) + .find(|(specifier, _)| { + specifier + .as_ref() + .is_ok_and(|x| x.syntax() == declaration.syntax()) + }) + .and_then(|(_, separator)| separator); + if let Some(separator) = following_separator { + mutation.remove_token(separator); + } + mutation.remove_node(declaration); + } + } + AnyJsBindingDeclaration::JsDefaultImportSpecifier(_) => { + mutation.remove_node(declaration); + } + AnyJsBindingDeclaration::JsNamespaceImportSpecifier(_) => { + let import_clause = JsImportNamedClause::cast(declaration.syntax().parent()?)?; + remove_named_import_from_import_clause(&mut mutation, import_clause).ok()?; + } + AnyJsBindingDeclaration::TsImportEqualsDeclaration(_) => { + mutation.remove_node(declaration); + } + _ => { + return None; + } + } + Some(JsRuleAction { + mutation, + category: ActionCategory::QuickFix, + applicability: Applicability::Always, + message: markup! { "Remove the unused import." }.to_owned(), + }) + } +} + +fn remove_named_import_from_import_clause( + mutation: &mut BatchMutation, + import_clause: JsImportNamedClause, +) -> SyntaxResult<()> { + if let Some(default_specifier) = import_clause.default_specifier() { + let default_clause = make::js_import_default_clause( + default_specifier.local_name()?, + make::token_decorated_with_space(T![from]), + import_clause.source()?, + ) + .build(); + mutation.replace_node( + AnyJsImportClause::from(import_clause), + default_clause.into(), + ); + } else if let Some(import) = import_clause.syntax().parent() { + transfer_leading_trivia_to_sibling(mutation, &import); + mutation.remove_element(NodeOrToken::Node(import)); + } + Ok(()) +} + +fn transfer_leading_trivia_to_sibling( + mutation: &mut BatchMutation, + node: &JsSyntaxNode, +) -> Option<()> { + node.first_leading_trivia(); + let pieces = node.first_leading_trivia()?.pieces(); + let (sibling, new_sibling) = if let Some(next_sibling) = node.next_sibling() { + let new_next_sibling = next_sibling.clone().prepend_trivia_pieces(pieces)?; + (next_sibling, new_next_sibling) + } else if let Some(prev_sibling) = node.prev_sibling() { + let new_prev_sibling = prev_sibling.clone().append_trivia_pieces(pieces)?; + (prev_sibling, new_prev_sibling) + } else { + return None; + }; + mutation + .replace_element_discard_trivia(NodeOrToken::Node(sibling), NodeOrToken::Node(new_sibling)); + Some(()) +} + +fn is_import(declaration: &AnyJsBindingDeclaration) -> bool { + matches!( + declaration, + AnyJsBindingDeclaration::JsBogusNamedImportSpecifier(_) + | AnyJsBindingDeclaration::JsDefaultImportSpecifier(_) + | AnyJsBindingDeclaration::JsImportDefaultClause(_) + | AnyJsBindingDeclaration::JsImportNamespaceClause(_) + | AnyJsBindingDeclaration::JsNamedImportSpecifier(_) + | AnyJsBindingDeclaration::JsNamespaceImportSpecifier(_) + | AnyJsBindingDeclaration::JsShorthandNamedImportSpecifier(_) + | AnyJsBindingDeclaration::TsImportEqualsDeclaration(_) + ) +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.js new file mode 100644 index 000000000000..78abe58a88c6 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.js @@ -0,0 +1,24 @@ +// Header comment +import A from "mod"; + +// Header comment +import * as B from "mod"; // Import comment + +// Header comment +import { C } from "mod"; // Import comment + +// Header comment +import /*a*/ D /*b*/, /*c*/{ E }/*d*/ from "mod"; // Import comment + +import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + +import { + // Comment + H, + I, +} from "mod"; + +import {/*a*/J/*b*/, /*c*/K/*d*/} from "mod"; + +// Header comment +import { L as M, } from "mod"; // Import comment diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.js.snap new file mode 100644 index 000000000000..d1fd37b03efe --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.js.snap @@ -0,0 +1,291 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```js +// Header comment +import A from "mod"; + +// Header comment +import * as B from "mod"; // Import comment + +// Header comment +import { C } from "mod"; // Import comment + +// Header comment +import /*a*/ D /*b*/, /*c*/{ E }/*d*/ from "mod"; // Import comment + +import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + +import { + // Comment + H, + I, +} from "mod"; + +import {/*a*/J/*b*/, /*c*/K/*d*/} from "mod"; + +// Header comment +import { L as M, } from "mod"; // Import comment + +``` + +# Diagnostics +``` +invalid.js:2:8 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 1 │ // Header comment + > 2 │ import A from "mod"; + │ ^ + 3 │ + 4 │ // Header comment + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 2 │ import·A·from·"mod"; + │ -------------------- + +``` + +``` +invalid.js:5:13 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 4 │ // Header comment + > 5 │ import * as B from "mod"; // Import comment + │ ^ + 6 │ + 7 │ // Header comment + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 5 │ import·*·as·B·from·"mod";·//·Import·comment + │ ------------------------------------------- + +``` + +``` +invalid.js:8:10 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 7 │ // Header comment + > 8 │ import { C } from "mod"; // Import comment + │ ^ + 9 │ + 10 │ // Header comment + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 8 │ import·{·C·}·from·"mod";·//·Import·comment + │ ------------------------------------------ + +``` + +``` +invalid.js:11:14 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 10 │ // Header comment + > 11 │ import /*a*/ D /*b*/, /*c*/{ E }/*d*/ from "mod"; // Import comment + │ ^ + 12 │ + 13 │ import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 11 │ import·/*a*/·D·/*b*/,·/*c*/{·E·}/*d*/·from·"mod";·//·Import·comment + │ -------------- + +``` + +``` +invalid.js:11:30 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 10 │ // Header comment + > 11 │ import /*a*/ D /*b*/, /*c*/{ E }/*d*/ from "mod"; // Import comment + │ ^ + 12 │ + 13 │ import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 11 │ import·/*a*/·D·/*b*/,·/*c*/{·E·}/*d*/·from·"mod";·//·Import·comment + │ ----------------- + +``` + +``` +invalid.js:13:14 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 11 │ import /*a*/ D /*b*/, /*c*/{ E }/*d*/ from "mod"; // Import comment + 12 │ + > 13 │ import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + │ ^ + 14 │ + 15 │ import { + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 13 │ import·/*a*/·F·/*b*/,·/*c*/·*·as·G·/*d*/·from·"mod"; + │ --------------- + +``` + +``` +invalid.js:13:34 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 11 │ import /*a*/ D /*b*/, /*c*/{ E }/*d*/ from "mod"; // Import comment + 12 │ + > 13 │ import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + │ ^ + 14 │ + 15 │ import { + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 13 │ import·/*a*/·F·/*b*/,·/*c*/·*·as·G·/*d*/·from·"mod"; + │ -------------------- + +``` + +``` +invalid.js:17:5 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 15 │ import { + 16 │ // Comment + > 17 │ H, + │ ^ + 18 │ I, + 19 │ } from "mod"; + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 14 14 │ + 15 15 │ import { + 16 │ - ····//·Comment + 17 │ - ····H, + 18 │ - ····I, + 16 │ + ····I, + 19 17 │ } from "mod"; + 20 18 │ + + +``` + +``` +invalid.js:18:5 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 16 │ // Comment + 17 │ H, + > 18 │ I, + │ ^ + 19 │ } from "mod"; + 20 │ + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 16 16 │ // Comment + 17 17 │ H, + 18 │ - ····I, + 19 18 │ } from "mod"; + 20 19 │ + + +``` + +``` +invalid.js:21:14 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 19 │ } from "mod"; + 20 │ + > 21 │ import {/*a*/J/*b*/, /*c*/K/*d*/} from "mod"; + │ ^ + 22 │ + 23 │ // Header comment + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 21 │ import·{/*a*/J/*b*/,·/*c*/K/*d*/}·from·"mod"; + │ ------------- + +``` + +``` +invalid.js:21:27 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 19 │ } from "mod"; + 20 │ + > 21 │ import {/*a*/J/*b*/, /*c*/K/*d*/} from "mod"; + │ ^ + 22 │ + 23 │ // Header comment + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 21 │ import·{/*a*/J/*b*/,·/*c*/K/*d*/}·from·"mod"; + │ ------ + +``` + +``` +invalid.js:24:15 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 23 │ // Header comment + > 24 │ import { L as M, } from "mod"; // Import comment + │ ^ + 25 │ + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 24 │ import·{·L·as·M,·}·from·"mod";·//·Import·comment + │ ------------------------------------------------ + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.jsx new file mode 100644 index 000000000000..dc4616aa192b --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.jsx @@ -0,0 +1,12 @@ +import X from "react" +import * as X from "react" +import { default as X } from "react" + +import React from "x" +import * as React from "x" +import { default as React } from "x" +import React, { useEffect } from "x" + +// unsupported patterns +import X, { default as React } from "react" +import X, * as React from "react" diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.jsx.snap new file mode 100644 index 000000000000..78dd2a08f0d5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.jsx.snap @@ -0,0 +1,267 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.jsx +--- +# Input +```js +import X from "react" +import * as X from "react" +import { default as X } from "react" + +import React from "x" +import * as React from "x" +import { default as React } from "x" +import React, { useEffect } from "x" + +// unsupported patterns +import X, { default as React } from "react" +import X, * as React from "react" + +``` + +# Diagnostics +``` +invalid.jsx:1:8 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + > 1 │ import X from "react" + │ ^ + 2 │ import * as X from "react" + 3 │ import { default as X } from "react" + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 1 │ import·X·from·"react" + │ --------------------- + +``` + +``` +invalid.jsx:2:13 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 1 │ import X from "react" + > 2 │ import * as X from "react" + │ ^ + 3 │ import { default as X } from "react" + 4 │ + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 2 │ import·*·as·X·from·"react" + │ -------------------------- + +``` + +``` +invalid.jsx:3:21 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 1 │ import X from "react" + 2 │ import * as X from "react" + > 3 │ import { default as X } from "react" + │ ^ + 4 │ + 5 │ import React from "x" + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 3 │ import·{·default·as·X·}·from·"react" + │ ------------------------------------ + +``` + +``` +invalid.jsx:5:8 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 3 │ import { default as X } from "react" + 4 │ + > 5 │ import React from "x" + │ ^^^^^ + 6 │ import * as React from "x" + 7 │ import { default as React } from "x" + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 5 │ import·React·from·"x" + │ --------------------- + +``` + +``` +invalid.jsx:6:13 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 5 │ import React from "x" + > 6 │ import * as React from "x" + │ ^^^^^ + 7 │ import { default as React } from "x" + 8 │ import React, { useEffect } from "x" + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 6 │ import·*·as·React·from·"x" + │ -------------------------- + +``` + +``` +invalid.jsx:7:21 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 5 │ import React from "x" + 6 │ import * as React from "x" + > 7 │ import { default as React } from "x" + │ ^^^^^ + 8 │ import React, { useEffect } from "x" + 9 │ + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 7 │ import·{·default·as·React·}·from·"x" + │ ------------------------------------ + +``` + +``` +invalid.jsx:8:8 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 6 │ import * as React from "x" + 7 │ import { default as React } from "x" + > 8 │ import React, { useEffect } from "x" + │ ^^^^^ + 9 │ + 10 │ // unsupported patterns + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 8 │ import·React,·{·useEffect·}·from·"x" + │ ------- + +``` + +``` +invalid.jsx:8:17 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 6 │ import * as React from "x" + 7 │ import { default as React } from "x" + > 8 │ import React, { useEffect } from "x" + │ ^^^^^^^^^ + 9 │ + 10 │ // unsupported patterns + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 8 │ import·React,·{·useEffect·}·from·"x" + │ --------------- + +``` + +``` +invalid.jsx:11:8 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 10 │ // unsupported patterns + > 11 │ import X, { default as React } from "react" + │ ^ + 12 │ import X, * as React from "react" + 13 │ + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 11 │ import·X,·{·default·as·React·}·from·"react" + │ --- + +``` + +``` +invalid.jsx:11:24 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 10 │ // unsupported patterns + > 11 │ import X, { default as React } from "react" + │ ^^^^^ + 12 │ import X, * as React from "react" + 13 │ + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 11 │ import·X,·{·default·as·React·}·from·"react" + │ ---------------------- + +``` + +``` +invalid.jsx:12:8 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 10 │ // unsupported patterns + 11 │ import X, { default as React } from "react" + > 12 │ import X, * as React from "react" + │ ^ + 13 │ + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 12 │ import·X,·*·as·React·from·"react" + │ --- + +``` + +``` +invalid.jsx:12:16 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 10 │ // unsupported patterns + 11 │ import X, { default as React } from "react" + > 12 │ import X, * as React from "react" + │ ^^^^^ + 13 │ + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 12 │ import·X,·*·as·React·from·"react" + │ ------------ + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.ts new file mode 100644 index 000000000000..089b76afa53f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.ts @@ -0,0 +1,24 @@ +// Header comment +import type A from "mod"; + +// Header comment +import type * as B from "mod"; // Import comment + +// Header comment +import type { C } from "mod"; // Import comment + +// Header comment +import /*a*/ D /*b*/, /*c*/{ type E }/*d*/ from "mod"; // Import comment + +import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + +import { + // Comment + type H, + type I, +} from "mod"; + +import {/*a*/type J/*b*/, /*c*/type K/*d*/} from "mod"; + +// Header comment +import type { L as M, } from "mod"; // Import comment diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.ts.snap new file mode 100644 index 000000000000..0e49ab1dbced --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/invalid.ts.snap @@ -0,0 +1,291 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.ts +--- +# Input +```js +// Header comment +import type A from "mod"; + +// Header comment +import type * as B from "mod"; // Import comment + +// Header comment +import type { C } from "mod"; // Import comment + +// Header comment +import /*a*/ D /*b*/, /*c*/{ type E }/*d*/ from "mod"; // Import comment + +import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + +import { + // Comment + type H, + type I, +} from "mod"; + +import {/*a*/type J/*b*/, /*c*/type K/*d*/} from "mod"; + +// Header comment +import type { L as M, } from "mod"; // Import comment + +``` + +# Diagnostics +``` +invalid.ts:2:13 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 1 │ // Header comment + > 2 │ import type A from "mod"; + │ ^ + 3 │ + 4 │ // Header comment + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 2 │ import·type·A·from·"mod"; + │ ------------------------- + +``` + +``` +invalid.ts:5:18 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 4 │ // Header comment + > 5 │ import type * as B from "mod"; // Import comment + │ ^ + 6 │ + 7 │ // Header comment + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 5 │ import·type·*·as·B·from·"mod";·//·Import·comment + │ ------------------------------------------------ + +``` + +``` +invalid.ts:8:15 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 7 │ // Header comment + > 8 │ import type { C } from "mod"; // Import comment + │ ^ + 9 │ + 10 │ // Header comment + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 8 │ import·type·{·C·}·from·"mod";·//·Import·comment + │ ----------------------------------------------- + +``` + +``` +invalid.ts:11:14 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 10 │ // Header comment + > 11 │ import /*a*/ D /*b*/, /*c*/{ type E }/*d*/ from "mod"; // Import comment + │ ^ + 12 │ + 13 │ import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 11 │ import·/*a*/·D·/*b*/,·/*c*/{·type·E·}/*d*/·from·"mod";·//·Import·comment + │ -------------- + +``` + +``` +invalid.ts:11:35 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 10 │ // Header comment + > 11 │ import /*a*/ D /*b*/, /*c*/{ type E }/*d*/ from "mod"; // Import comment + │ ^ + 12 │ + 13 │ import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 11 │ import·/*a*/·D·/*b*/,·/*c*/{·type·E·}/*d*/·from·"mod";·//·Import·comment + │ ---------------------- + +``` + +``` +invalid.ts:13:14 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 11 │ import /*a*/ D /*b*/, /*c*/{ type E }/*d*/ from "mod"; // Import comment + 12 │ + > 13 │ import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + │ ^ + 14 │ + 15 │ import { + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 13 │ import·/*a*/·F·/*b*/,·/*c*/·*·as·G·/*d*/·from·"mod"; + │ --------------- + +``` + +``` +invalid.ts:13:34 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 11 │ import /*a*/ D /*b*/, /*c*/{ type E }/*d*/ from "mod"; // Import comment + 12 │ + > 13 │ import /*a*/ F /*b*/, /*c*/ * as G /*d*/ from "mod"; + │ ^ + 14 │ + 15 │ import { + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 13 │ import·/*a*/·F·/*b*/,·/*c*/·*·as·G·/*d*/·from·"mod"; + │ -------------------- + +``` + +``` +invalid.ts:17:10 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 15 │ import { + 16 │ // Comment + > 17 │ type H, + │ ^ + 18 │ type I, + 19 │ } from "mod"; + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 14 14 │ + 15 15 │ import { + 16 │ - ····//·Comment + 17 │ - ····type·H, + 18 │ - ····type·I, + 16 │ + ····type·I, + 19 17 │ } from "mod"; + 20 18 │ + + +``` + +``` +invalid.ts:18:10 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 16 │ // Comment + 17 │ type H, + > 18 │ type I, + │ ^ + 19 │ } from "mod"; + 20 │ + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 16 16 │ // Comment + 17 17 │ type H, + 18 │ - ····type·I, + 19 18 │ } from "mod"; + 20 19 │ + + +``` + +``` +invalid.ts:21:19 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 19 │ } from "mod"; + 20 │ + > 21 │ import {/*a*/type J/*b*/, /*c*/type K/*d*/} from "mod"; + │ ^ + 22 │ + 23 │ // Header comment + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 21 │ import·{/*a*/type·J/*b*/,·/*c*/type·K/*d*/}·from·"mod"; + │ ------------------ + +``` + +``` +invalid.ts:21:37 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 19 │ } from "mod"; + 20 │ + > 21 │ import {/*a*/type J/*b*/, /*c*/type K/*d*/} from "mod"; + │ ^ + 22 │ + 23 │ // Header comment + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 21 │ import·{/*a*/type·J/*b*/,·/*c*/type·K/*d*/}·from·"mod"; + │ ----------- + +``` + +``` +invalid.ts:24:20 lint/nursery/noUnusedImports FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This import is unused. + + 23 │ // Header comment + > 24 │ import type { L as M, } from "mod"; // Import comment + │ ^ + 25 │ + + i Unused imports usually are result of incomplete refactoring. + + i Safe fix: Remove the unused import. + + 24 │ import·type·{·L·as·M,·}·from·"mod";·//·Import·comment + │ ----------------------------------------------------- + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.js new file mode 100644 index 000000000000..3245360767dc --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.js @@ -0,0 +1,10 @@ +import A from "mod"; +import * as B from "mod"; +import { C } from "mod"; +import D, { E } from "mod"; +import F, * as G from "mod"; +import { H, I } from "mod"; +import { J, K } from "mod"; +import { L, } from "mod"; + +export { A, B, C, D, E, F, G, H, I, J, K, L }; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.js.snap new file mode 100644 index 000000000000..b6c3ccab8d62 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.js.snap @@ -0,0 +1,20 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```js +import A from "mod"; +import * as B from "mod"; +import { C } from "mod"; +import D, { E } from "mod"; +import F, * as G from "mod"; +import { H, I } from "mod"; +import { J, K } from "mod"; +import { L, } from "mod"; + +export { A, B, C, D, E, F, G, H, I, J, K, L }; + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.jsx new file mode 100644 index 000000000000..072d426f941c --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.jsx @@ -0,0 +1,4 @@ +import React from "react" +import * as React from "react" +import { default as React } from "react" +import React, { useEffect } from "react" \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.jsx.snap new file mode 100644 index 000000000000..923660f2873c --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUnusedImports/valid.jsx.snap @@ -0,0 +1,13 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.jsx +--- +# Input +```js +import React from "react" +import * as React from "react" +import { default as React } from "react" +import React, { useEffect } from "react" +``` + + diff --git a/crates/biome_js_syntax/src/identifier_ext.rs b/crates/biome_js_syntax/src/identifier_ext.rs index 5d4884ff043c..ba8a56b3d55b 100644 --- a/crates/biome_js_syntax/src/identifier_ext.rs +++ b/crates/biome_js_syntax/src/identifier_ext.rs @@ -1,4 +1,7 @@ -use crate::{JsIdentifierAssignment, JsReferenceIdentifier, JsSyntaxToken, JsxReferenceIdentifier}; +use crate::{ + JsIdentifierAssignment, JsLiteralExportName, JsReferenceIdentifier, JsSyntaxToken, + JsxReferenceIdentifier, +}; use biome_rowan::{declare_node_union, SyntaxResult}; declare_node_union! { @@ -14,3 +17,9 @@ impl AnyJsIdentifierUsage { } } } + +impl JsLiteralExportName { + pub fn is_default(&self) -> SyntaxResult { + Ok(self.value()?.text_trimmed() == "default") + } +} diff --git a/crates/biome_service/src/configuration/linter/rules.rs b/crates/biome_service/src/configuration/linter/rules.rs index cfa60b563cc9..32aec0a6ce7d 100644 --- a/crates/biome_service/src/configuration/linter/rules.rs +++ b/crates/biome_service/src/configuration/linter/rules.rs @@ -2219,6 +2219,10 @@ pub struct Nursery { )] #[serde(skip_serializing_if = "Option::is_none")] pub no_misleading_instantiator: Option, + #[doc = "Disallow unused imports."] + #[bpaf(long("no-unused-imports"), argument("on|off|warn"), optional, hide)] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_unused_imports: Option, #[doc = "Disallow else block when the if block breaks early."] #[bpaf(long("no-useless-else"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] @@ -2273,7 +2277,7 @@ pub struct Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 18] = [ + pub(crate) const GROUP_RULES: [&'static str; 19] = [ "noAccumulatingSpread", "noConfusingVoidType", "noDuplicateJsonKeys", @@ -2283,6 +2287,7 @@ impl Nursery { "noGlobalIsNan", "noInvalidNewBuiltin", "noMisleadingInstantiator", + "noUnusedImports", "noUselessElse", "noVoid", "useArrowFunction", @@ -2311,13 +2316,13 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 18] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 19] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -2336,6 +2341,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended(&self) -> bool { @@ -2397,51 +2403,56 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_useless_else.as_ref() { + if let Some(rule) = self.no_unused_imports.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_void.as_ref() { + if let Some(rule) = self.no_useless_else.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.use_arrow_function.as_ref() { + if let Some(rule) = self.no_void.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.use_collapsed_else_if.as_ref() { + if let Some(rule) = self.use_arrow_function.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_collapsed_else_if.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.use_is_array.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } + if let Some(rule) = self.use_is_array.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -2491,51 +2502,56 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_useless_else.as_ref() { + if let Some(rule) = self.no_unused_imports.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_void.as_ref() { + if let Some(rule) = self.no_useless_else.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.use_arrow_function.as_ref() { + if let Some(rule) = self.no_void.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.use_collapsed_else_if.as_ref() { + if let Some(rule) = self.use_arrow_function.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_collapsed_else_if.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.use_is_array.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } + if let Some(rule) = self.use_is_array.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -2549,7 +2565,7 @@ impl Nursery { pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 10] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 18] { + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 19] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] @@ -2581,6 +2597,7 @@ impl Nursery { "noGlobalIsNan" => self.no_global_is_nan.as_ref(), "noInvalidNewBuiltin" => self.no_invalid_new_builtin.as_ref(), "noMisleadingInstantiator" => self.no_misleading_instantiator.as_ref(), + "noUnusedImports" => self.no_unused_imports.as_ref(), "noUselessElse" => self.no_useless_else.as_ref(), "noVoid" => self.no_void.as_ref(), "useArrowFunction" => self.use_arrow_function.as_ref(), diff --git a/crates/biome_service/src/configuration/parse/json/rules.rs b/crates/biome_service/src/configuration/parse/json/rules.rs index e27430db59ff..bb88a380a557 100644 --- a/crates/biome_service/src/configuration/parse/json/rules.rs +++ b/crates/biome_service/src/configuration/parse/json/rules.rs @@ -2032,6 +2032,7 @@ impl VisitNode for Nursery { "noGlobalIsNan", "noInvalidNewBuiltin", "noMisleadingInstantiator", + "noUnusedImports", "noUselessElse", "noVoid", "useArrowFunction", @@ -2267,6 +2268,29 @@ impl VisitNode for Nursery { )); } }, + "noUnusedImports" => match value { + AnyJsonValue::JsonStringValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_known_string(&value, name_text, &mut configuration, diagnostics)?; + self.no_unused_imports = Some(configuration); + } + AnyJsonValue::JsonObjectValue(_) => { + let mut rule_configuration = RuleConfiguration::default(); + rule_configuration.map_rule_configuration( + &value, + name_text, + "noUnusedImports", + diagnostics, + )?; + self.no_unused_imports = Some(rule_configuration); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + "object or string", + value.range(), + )); + } + }, "noUselessElse" => match value { AnyJsonValue::JsonStringValue(_) => { let mut configuration = RuleConfiguration::default(); diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index a05cb490946e..eb43cf57e90b 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -1049,6 +1049,13 @@ { "type": "null" } ] }, + "noUnusedImports": { + "description": "Disallow unused imports.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUselessElse": { "description": "Disallow else block when the if block breaks early.", "anyOf": [ diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 0f76ea46c07e..89a174f4b76e 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -701,6 +701,10 @@ export interface Nursery { * Enforce proper usage of new and constructor. */ noMisleadingInstantiator?: RuleConfiguration; + /** + * Disallow unused imports. + */ + noUnusedImports?: RuleConfiguration; /** * Disallow else block when the if block breaks early. */ @@ -1300,6 +1304,7 @@ export type Category = | "lint/nursery/noGlobalIsNan" | "lint/nursery/noInvalidNewBuiltin" | "lint/nursery/noMisleadingInstantiator" + | "lint/nursery/noUnusedImports" | "lint/nursery/noUselessElse" | "lint/nursery/noVoid" | "lint/nursery/useArrowFunction" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index a05cb490946e..eb43cf57e90b 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1049,6 +1049,13 @@ { "type": "null" } ] }, + "noUnusedImports": { + "description": "Disallow unused imports.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noUselessElse": { "description": "Disallow else block when the if block breaks early.", "anyOf": [ diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro index d9ea0fa6e034..90c4452ed497 100644 --- a/website/src/components/generated/NumberOfRules.astro +++ b/website/src/components/generated/NumberOfRules.astro @@ -1,2 +1,2 @@ -

Biome's linter has a total of 160 rules

\ No newline at end of file +

Biome's linter has a total of 161 rules

\ No newline at end of file diff --git a/website/src/content/docs/internals/changelog.mdx b/website/src/content/docs/internals/changelog.mdx index 52326e10442e..b9327f63e71f 100644 --- a/website/src/content/docs/internals/changelog.mdx +++ b/website/src/content/docs/internals/changelog.mdx @@ -73,6 +73,14 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom The rule reports `else` clauses that can be omitted because their `if` branches break. Contributed by @Conaclos +- Add [noUnusedImports](https://biomejs.dev/linter/rules/no-unused-imports) rule. + The rule reports unused imports and suggests to remove them. + Contributed by @Conaclos + + [noUnusedVariables](https://biomejs.dev/linter/rules/no-unused-variables) reports also unused imports, but don't suggest their removal. + Once [noUnusedImports](https://biomejs.dev/linter/rules/no-unused-imports) stabilized, + [noUnusedVariables](https://biomejs.dev/linter/rules/no-unused-variables) will not report unused imports. + #### Enhancements - The following rules have now safe code fixes: diff --git a/website/src/content/docs/linter/rules/index.mdx b/website/src/content/docs/linter/rules/index.mdx index aa46aadae4fe..b62b836a4a19 100644 --- a/website/src/content/docs/linter/rules/index.mdx +++ b/website/src/content/docs/linter/rules/index.mdx @@ -213,6 +213,7 @@ Rules that belong to this group are not subject to semantic versionNumber.isNaN instead of global isNaN. | ⚠️ | | [noInvalidNewBuiltin](/linter/rules/no-invalid-new-builtin) | Disallow new operators with global non-constructor functions. | ⚠️ | | [noMisleadingInstantiator](/linter/rules/no-misleading-instantiator) | Enforce proper usage of new and constructor. | | +| [noUnusedImports](/linter/rules/no-unused-imports) | Disallow unused imports. | | | [noUselessElse](/linter/rules/no-useless-else) | Disallow else block when the if block breaks early. | ⚠️ | | [noVoid](/linter/rules/no-void) | Disallow the use of void operators, which is not a familiar operator. | | | [useArrowFunction](/linter/rules/use-arrow-function) | Use arrow functions over function expressions. | 🔧 | diff --git a/website/src/content/docs/linter/rules/no-unused-imports.md b/website/src/content/docs/linter/rules/no-unused-imports.md new file mode 100644 index 000000000000..8c17b114b873 --- /dev/null +++ b/website/src/content/docs/linter/rules/no-unused-imports.md @@ -0,0 +1,120 @@ +--- +title: noUnusedImports (since vnext) +--- + +**Diagnostic Category: `lint/nursery/noUnusedImports`** + +:::caution +This rule is part of the [nursery](/linter/rules/#nursery) group. +::: + +Disallow unused imports. + +Unused imports usually are result of incomplete refactoring. + +There is one exception to the rule: the `React` import. +Importing the `React` variable was a mandatory pattern until some time ago: +For the time being this rule will ignore it, +but this **might change in the future releases**. + +## Examples + +### Invalid + +```jsx +// Comment +import A from 'mod'; +``` + +

nursery/noUnusedImports.js:2:8 lint/nursery/noUnusedImports  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   This import is unused.
+  
+    1 │ // Comment
+  > 2 │ import A from 'mod';
+          ^
+    3 │ 
+  
+   Unused imports usually are result of incomplete refactoring.
+  
+   Safe fix: Remove the unused import.
+  
+    1  - //·Comment
+    2  - import·A·from·'mod';
+      1+ 
+    3 2  
+  
+
+ +```jsx +// Comment +import * as A from 'mod'; +``` + +
nursery/noUnusedImports.js:2:13 lint/nursery/noUnusedImports  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   This import is unused.
+  
+    1 │ // Comment
+  > 2 │ import * as A from 'mod';
+               ^
+    3 │ 
+  
+   Unused imports usually are result of incomplete refactoring.
+  
+   Safe fix: Remove the unused import.
+  
+    1  - //·Comment
+    2  - import·*·as·A·from·'mod';
+      1+ 
+    3 2  
+  
+
+ +```ts +import { type A, B } from 'mod'; + +export { B } +``` + +
nursery/noUnusedImports.js:1:15 lint/nursery/noUnusedImports  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   This import is unused.
+  
+  > 1 │ import { type A, B } from 'mod';
+                 ^
+    2 │ 
+    3 │ export { B }
+  
+   Unused imports usually are result of incomplete refactoring.
+  
+   Safe fix: Remove the unused import.
+  
+    1 │ import·{·type·A,·B·}·from·'mod';
+           --------               
+
+ +## Valid + +```ts +import { A, type B } from 'mod'; + +function f(arg: B): A { + return new A(arg); +} +``` + +```jsx +import React from 'react'; + +function foo() { + return
; +}; + +foo(); +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)