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

Add a new lint rule `useDisposables` for JavaScript, which detects disposable objects assigned to variables without `using` or `await using` syntax. Disposable objects that implement the `Disposable` or `AsyncDisposable` interface are intended to be disposed of after use. Not disposing them can lead to resource or memory leaks, depending on the implementation.

**Invalid:**

```js
function createDisposable(): Disposable {
return {
[Symbol.dispose]() {
// do something
},
};
}

const disposable = createDisposable();
```

**Valid:**

```js
function createDisposable(): Disposable {
return {
[Symbol.dispose]() {
// do something
},
};
}

using disposable = createDisposable();
```
4 changes: 4 additions & 0 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

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

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

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

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs

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

162 changes: 162 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/use_disposables.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use crate::JsRuleAction;
use crate::services::typed::Typed;
use biome_analyze::{
FixKind, Rule, RuleDiagnostic, RuleDomain, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_js_factory::make;
use biome_js_syntax::{JsVariableDeclaration, JsVariableDeclarator, JsVariableDeclaratorList, T};
use biome_rowan::{AstNode, BatchMutationExt};
use biome_rule_options::use_disposables::UseDisposablesOptions;

declare_lint_rule! {
/// Detects a disposable object assigned to a variable without using or await using syntax.
///
/// Disposable objects, which implements Disposable or AsyncDisposable interface, are intended
/// to dispose after use. Not disposing them can lead some resource or memory leak depending on
/// the implementation.
///
/// ## Examples
///
/// ### Invalid
///
/// ```ts,expect_diagnostic,file=example1.ts
/// function createDisposable(): Disposable {
/// return {
/// [Symbol.dispose]() {
/// // do something
/// },
/// };
/// }
///
/// const disposable = createDisposable();
/// ```
///
/// ```ts,expect_diagnostic,file=example2.ts
/// class MyClass implements AsyncDisposable {
/// async [Symbol.asyncDispose]() {
/// // do something
/// }
/// }
///
/// const instance = new MyClass();
/// ```
///
/// ### Valid
///
/// ```ts,file=example3.ts
/// function createDisposable(): Disposable {
/// return {
/// [Symbol.dispose]() {
/// // do something
/// },
/// };
/// }
///
/// using disposable = createDisposable();
/// ```
///
/// ```ts,file=example4.ts
/// class MyClass implements AsyncDisposable {
/// async [Symbol.asyncDispose]() {
/// // do something
/// }
/// }
///
/// await using instance = new MyClass();
/// ```
///
pub UseDisposables {
version: "next",
name: "useDisposables",
language: "js",
recommended: false,
fix_kind: FixKind::Unsafe,
domains: &[RuleDomain::Types],
}
}

impl Rule for UseDisposables {
type Query = Typed<JsVariableDeclarator>;
type State = DisposableKind;
type Signals = Option<Self::State>;
type Options = UseDisposablesOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let decl = ctx.query();
let initializer = decl.initializer()?;
let expression = initializer.expression().ok()?;
let ty = ctx.type_of_expression(&expression);

// Lookup the parent declaration which possibly has `await` and/or `using` tokens.
let parent = decl
.parent::<JsVariableDeclaratorList>()?
.parent::<JsVariableDeclaration>()?;

let is_disposed = parent.kind().ok()?.kind() == T![using];
if ty.is_disposable() && !is_disposed {
return Some(DisposableKind::Disposable);
}

let is_async_disposed = is_disposed && parent.await_token().is_some();
if ty.is_async_disposable() && !is_async_disposed {
return Some(DisposableKind::AsyncDisposable);
}

None
}

fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();

Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! { "Disposable object is assigned here but never disposed." },
)
.note(match state {
DisposableKind::Disposable => markup! {
"The object implements the "<Emphasis>"Disposable"</Emphasis>" interface, which is intended to be disposed after use with "<Emphasis>"using"</Emphasis>" syntax."
},
DisposableKind::AsyncDisposable => markup! {
"The object implements the "<Emphasis>"AsyncDisposable"</Emphasis>" interface, which is intended to be disposed after use with "<Emphasis>"await using"</Emphasis>" syntax."
},
})
.note(markup! {
"Not disposing the object properly can lead some resource or memory leak."
})
)
}

fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
let mut mutation = ctx.root().begin();

let decl = ctx
.query()
.parent::<JsVariableDeclaratorList>()?
.parent::<JsVariableDeclaration>()?;

let mut new_decl = decl
.clone()
.with_kind_token(make::token_with_trailing_space(T![using]));

if let DisposableKind::AsyncDisposable = state {
new_decl = new_decl.with_await_token(Some(make::token_with_trailing_space(T![await])));
}

mutation.replace_node(decl, new_decl);

Some(JsRuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
markup! { "Add the "<Emphasis>"using"</Emphasis>" keyword to dispose the object when leaving the scope." },
mutation,
))
}
}

pub enum DisposableKind {
Disposable,
AsyncDisposable,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* should generate diagnostics */
const disposable = {
[Symbol.dispose]() {
// do something
}
};

const asyncDisposable = {
async [Symbol.asyncDispose]() {
// do something
}
};

function createDisposable(): Disposable {
return {
[Symbol.dispose]() {
// do something
},
};
}

const createdDisposable = createDisposable();

function createAsyncDisposable(): AsyncDisposable {
return {
async [Symbol.asyncDispose](): Promise<void> {
// do something
},
};
}

const createdAsyncDisposable = createAsyncDisposable();

class DisposableClass implements Disposable {
[Symbol.dispose](): void {
// do something
}
}

const disposableInstance = new DisposableClass();

class AsyncDisposableClass implements AsyncDisposable {
async [Symbol.asyncDispose](): Promise<void> {
// do something
}
}

const asyncDisposableInstance = new AsyncDisposableClass();
Loading
Loading