Skip to content

Commit

Permalink
feat(lint/noUnusedImports): add ignoreReact option (#2306)
Browse files Browse the repository at this point in the history
  • Loading branch information
Conaclos authored Apr 4, 2024
1 parent ed994f4 commit 2bd95dc
Show file tree
Hide file tree
Showing 13 changed files with 404 additions and 14 deletions.
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,21 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b

### Linter

#### Bug fixes
#### New features

- Lint rules `useNodejsImportProtocol`, `useNodeAssertStrict`, `noRestrictedImports`, `noNodejsModules` will no longer check `declare module` statements anymore. Contributed by @Sec-ant
- Add a new option `ignoreReact` to [noUnusedImports](https://biomejs.dev/linter/rules/no-unused-imports).

#### New features
When `ignoreReact` is enabled, Biome ignores imports of `React` from the `react` package.
The option is disabled by default.

Contributed by @Conaclos

#### Enhancements

#### Bug fixes

- Lint rules `useNodejsImportProtocol`, `useNodeAssertStrict`, `noRestrictedImports`, `noNodejsModules` will no longer check `declare module` statements anymore. Contributed by @Sec-ant

### Parser


Expand Down
44 changes: 41 additions & 3 deletions crates/biome_js_analyze/src/lint/correctness/no_unused_imports.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
use crate::{services::semantic::Semantic, JsRuleAction};
use crate::{
react::{is_global_react_import, ReactLibrary},
services::semantic::Semantic,
JsRuleAction,
};
use biome_analyze::{
context::RuleContext, declare_rule, ActionCategory, FixKind, Rule, RuleDiagnostic,
};
use biome_console::markup;
use biome_deserialize_macros::Deserializable;
use biome_diagnostics::Applicability;
use biome_js_factory::make;
use biome_js_semantic::ReferencesExtensions;
Expand All @@ -11,6 +16,10 @@ use biome_js_syntax::{
JsIdentifierBinding, JsImport, JsLanguage, JsNamedImportSpecifierList, JsSyntaxNode, T,
};
use biome_rowan::{AstNode, AstSeparatedList, BatchMutation, BatchMutationExt};
use serde::{Deserialize, Serialize};

#[cfg(feature = "schemars")]
use schemars::JsonSchema;

declare_rule! {
/// Disallow unused imports.
Expand All @@ -23,6 +32,25 @@ declare_rule! {
/// the unused imports will also be removed. So that comment directives
/// like `@ts-expect-error` won't be transferred to a wrong place.
///
/// ## Options
///
/// The rule provides a single option `ignoreReact`.
/// When this option is set to `true`, imports named `React` from the package `react` are ignored.
/// `ignoreReact` is disabled by default.
///
/// ```json
/// {
/// "//": "...",
/// "options": {
/// "ignoreReact": true
/// }
/// }
/// ```
///
/// This option should only be necessary if you cannot upgrade to a React version that supports the new JSX runtime.
/// In the new JSX runtime, you no longer need to import `React`.
/// You can find more details in [this comment](https://github.com/biomejs/biome/issues/571#issuecomment-1774026734).
///
/// ## Examples
///
/// ### Invalid
Expand Down Expand Up @@ -77,15 +105,17 @@ impl Rule for NoUnusedImports {
type Query = Semantic<JsIdentifierBinding>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();
type Options = UnusedImportsOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let binding = ctx.query();
let declaration = binding.declaration()?;
if !is_import(&declaration) {
return None;
}

if ctx.options().ignore_react && is_global_react_import(binding, ReactLibrary::React) {
return None;
}
let model = ctx.model();
binding.all_references(model).next().is_none().then_some(())
}
Expand Down Expand Up @@ -152,6 +182,14 @@ impl Rule for NoUnusedImports {
}
}

#[derive(Clone, Debug, Default, Deserializable, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UnusedImportsOptions {
/// Ignore `React` imports from the `react` package when set to `true`.
ignore_react: bool,
}

fn remove_import_specifier(
mutation: &mut BatchMutation<JsLanguage>,
specifier: &JsSyntaxNode,
Expand Down
41 changes: 38 additions & 3 deletions crates/biome_js_analyze/src/react.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ pub mod hooks;

use biome_js_semantic::{Binding, SemanticModel};
use biome_js_syntax::{
AnyJsCallArgument, AnyJsExpression, AnyJsMemberExpression, AnyJsNamedImportSpecifier,
AnyJsObjectMember, JsCallExpression, JsIdentifierBinding, JsImport, JsObjectExpression,
JsPropertyObjectMember, JsxMemberName, JsxReferenceIdentifier,
binding_ext::AnyJsBindingDeclaration, AnyJsCallArgument, AnyJsExpression,
AnyJsMemberExpression, AnyJsNamedImportSpecifier, AnyJsObjectMember, JsCallExpression,
JsIdentifierBinding, JsImport, JsObjectExpression, JsPropertyObjectMember, JsxMemberName,
JsxReferenceIdentifier,
};
use biome_rowan::{AstNode, AstSeparatedList};

Expand Down Expand Up @@ -291,3 +292,37 @@ fn is_named_react_export(binding: &Binding, lib: ReactLibrary, name: &str) -> Op
let import = import_specifier.import_clause()?.parent::<JsImport>()?;
Some(import.source_text().ok()?.text() == lib.import_name())
}

/// Checks if `binding` is an import of the global name of `lib`.
pub(crate) fn is_global_react_import(binding: &JsIdentifierBinding, lib: ReactLibrary) -> bool {
if !binding
.name_token()
.is_ok_and(|name| name.text_trimmed() == lib.global_name())
{
return false;
};
let Some(decl) = binding.declaration() else {
return false;
};
// This must be a default import or a namespace import
let syntax = match decl {
AnyJsBindingDeclaration::JsNamedImportSpecifier(specifier) => {
if !specifier.name().is_ok_and(|name| name.is_default()) {
return false;
}
specifier.into_syntax()
}
AnyJsBindingDeclaration::JsDefaultImportSpecifier(specifier) => specifier.into_syntax(),
AnyJsBindingDeclaration::JsNamespaceImportSpecifier(specifier) => specifier.into_syntax(),
_ => {
return false;
}
};
// Check import source
syntax
.ancestors()
.skip(1)
.find_map(JsImport::cast)
.and_then(|import| import.source_text().ok())
.is_some_and(|source| source.text() == lib.import_name())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid-unused-react.jsx
---
# Input
```jsx
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"

```

# Diagnostics
```
invalid-unused-react.jsx:1:8 lint/correctness/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 might be the result of an incomplete refactoring.
i Safe fix: Remove the unused import.
1 │ import·X·from·"react"
│ ---------------------
```

```
invalid-unused-react.jsx:2:13 lint/correctness/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 might be the result of an incomplete refactoring.
i Safe fix: Remove the unused import.
1 1 │ import X from "react"
2 │ - import·*·as·X·from·"react"
3 2 │ import { default as X } from "react"
4 3 │
```

```
invalid-unused-react.jsx:3:21 lint/correctness/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 might be the result of an incomplete refactoring.
i Safe fix: Remove the unused import.
1 1 │ import X from "react"
2 2 │ import * as X from "react"
3 │ - import·{·default·as·X·}·from·"react"
4 3 │
5 4 │ import React from "x"
```

```
invalid-unused-react.jsx:5:8 lint/correctness/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 might be the result of an incomplete refactoring.
i Safe fix: Remove the unused import.
2 2 │ import * as X from "react"
3 3 │ import { default as X } from "react"
4 │ -
5 │ - import·React·from·"x"
6 4 │ import * as React from "x"
7 5 │ import { default as React } from "x"
```

```
invalid-unused-react.jsx:6:13 lint/correctness/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 might be the result of an incomplete refactoring.
i Safe fix: Remove the unused import.
4 4 │
5 5 │ import React from "x"
6 │ - import·*·as·React·from·"x"
7 6 │ import { default as React } from "x"
8 7 │ import React, { useEffect } from "x"
```

```
invalid-unused-react.jsx:7:21 lint/correctness/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 might be the result of an incomplete refactoring.
i Safe fix: Remove the unused import.
5 5 │ import React from "x"
6 6 │ import * as React from "x"
7 │ - import·{·default·as·React·}·from·"x"
8 7 │ import React, { useEffect } from "x"
9 8 │
```

```
invalid-unused-react.jsx:8:8 lint/correctness/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 │
i Unused imports might be the result of an incomplete refactoring.
i Safe fix: Remove the unused import.
8 │ import·React,·{·useEffect·}·from·"x"
│ -------
```

```
invalid-unused-react.jsx:8:17 lint/correctness/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 │
i Unused imports might be the result of an incomplete refactoring.
i Safe fix: Remove the unused import.
8 │ import·React,·{·useEffect·}·from·"x"
│ ---------------
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"linter": {
"rules": {
"correctness": {
"noUnusedImports": {
"level": "error",
"options": {
"ignoreReact": true
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from "react"
import { default as React } from "react"
import * as React from "react"
Loading

0 comments on commit 2bd95dc

Please sign in to comment.