Skip to content

Commit fa5b09b

Browse files
committed
feat(js/lint): implement rule useDisposables
1 parent 1556129 commit fa5b09b

File tree

15 files changed

+736
-74
lines changed

15 files changed

+736
-74
lines changed

.changeset/fast-glasses-ask.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Add a new lint rule `useDisposables` for JavaScript, which 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.
6+
7+
**Invalid:**
8+
9+
```js
10+
function createDisposable(): Disposable {
11+
return {
12+
[Symbol.dispose]() {
13+
// do something
14+
},
15+
};
16+
}
17+
18+
const disposable = createDisposable();
19+
```
20+
21+
**Valid:**
22+
23+
```js
24+
function createDisposable(): Disposable {
25+
return {
26+
[Symbol.dispose]() {
27+
// do something
28+
},
29+
};
30+
}
31+
32+
using disposable = createDisposable();
33+
```

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 91 additions & 70 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/generated/domain_selector.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub mod use_array_sort_compare;
4545
pub mod use_await_thenable;
4646
pub mod use_consistent_arrow_return;
4747
pub mod use_destructuring;
48+
pub mod use_disposables;
4849
pub mod use_exhaustive_switch_cases;
4950
pub mod use_explicit_type;
5051
pub mod use_find;
@@ -57,4 +58,4 @@ pub mod use_spread;
5758
pub mod use_vue_consistent_define_props_declaration;
5859
pub mod use_vue_define_macros_order;
5960
pub mod use_vue_multi_word_component_names;
60-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
61+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_disposables :: UseDisposables , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
use crate::JsRuleAction;
2+
use crate::services::typed::Typed;
3+
use biome_analyze::{
4+
FixKind, Rule, RuleDiagnostic, RuleDomain, context::RuleContext, declare_lint_rule,
5+
};
6+
use biome_console::markup;
7+
use biome_js_factory::make;
8+
use biome_js_syntax::{JsVariableDeclaration, JsVariableDeclarator, JsVariableDeclaratorList, T};
9+
use biome_rowan::{AstNode, BatchMutationExt};
10+
use biome_rule_options::use_disposables::UseDisposablesOptions;
11+
12+
declare_lint_rule! {
13+
/// Detects a disposable object assigned to a variable without using or await using syntax.
14+
///
15+
/// Disposable objects, which implements Disposable or AsyncDisposable interface, are intended
16+
/// to dispose after use. Not disposing them can lead some resource or memory leak depending on
17+
/// the implementation.
18+
///
19+
/// ## Examples
20+
///
21+
/// ### Invalid
22+
///
23+
/// ```ts,expect_diagnostic
24+
/// function createDisposable(): Disposable {
25+
/// return {
26+
/// [Symbol.dispose]() {
27+
/// // do something
28+
/// },
29+
/// };
30+
/// }
31+
///
32+
/// const disposable = createDisposable();
33+
/// ```
34+
///
35+
/// ```js,expect_diagnostic
36+
/// class MyClass implements AsyncDisposable {
37+
/// async [Symbol.asyncDispose]() {
38+
/// // do something
39+
/// },
40+
/// }
41+
///
42+
/// const instance = new MyClass();
43+
/// ```
44+
///
45+
/// ### Valid
46+
///
47+
/// ```ts
48+
/// function createDisposable(): Disposable {
49+
/// return {
50+
/// [Symbol.dispose]() {
51+
/// // do something
52+
/// },
53+
/// };
54+
/// }
55+
///
56+
/// using disposable = createDisposable();
57+
/// ```
58+
///
59+
/// ```js
60+
/// class MyClass implements AsyncDisposable {
61+
/// async [Symbol.asyncDispose]() {
62+
/// // do something
63+
/// },
64+
/// }
65+
///
66+
/// await using instance = new MyClass();
67+
/// ```
68+
///
69+
pub UseDisposables {
70+
version: "next",
71+
name: "useDisposables",
72+
language: "js",
73+
recommended: false,
74+
fix_kind: FixKind::Unsafe,
75+
domains: &[RuleDomain::Project],
76+
}
77+
}
78+
79+
impl Rule for UseDisposables {
80+
type Query = Typed<JsVariableDeclarator>;
81+
type State = DisposableKind;
82+
type Signals = Option<Self::State>;
83+
type Options = UseDisposablesOptions;
84+
85+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
86+
let decl = ctx.query();
87+
let initializer = decl.initializer()?;
88+
let expression = initializer.expression().ok()?;
89+
let ty = ctx.type_of_expression(&expression);
90+
91+
// Lookup the parent declaration which possibly has `await` and/or `using` tokens.
92+
let parent = decl
93+
.parent::<JsVariableDeclaratorList>()?
94+
.parent::<JsVariableDeclaration>()?;
95+
96+
let is_disposed = parent.kind().ok()?.kind() == T![using];
97+
if ty.is_disposable() && !is_disposed {
98+
return Some(DisposableKind::Disposable);
99+
}
100+
101+
let is_async_disposed = is_disposed && parent.await_token().is_some();
102+
if ty.is_async_disposable() && !is_async_disposed {
103+
return Some(DisposableKind::AsyncDisposable);
104+
}
105+
106+
None
107+
}
108+
109+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
110+
let node = ctx.query();
111+
112+
Some(
113+
RuleDiagnostic::new(
114+
rule_category!(),
115+
node.range(),
116+
markup! { "Disposable object is assigned here but never disposed." },
117+
)
118+
.note(match state {
119+
DisposableKind::Disposable => markup! {
120+
"The object implements the "<Emphasis>"Disposable"</Emphasis>"interface, which is intended to be disposed after use with "<Emphasis>"using"</Emphasis>" syntax."
121+
},
122+
DisposableKind::AsyncDisposable => markup! {
123+
"The object implements the "<Emphasis>"AsyncDisposable"</Emphasis>"interface, which is intended to be disposed after use with "<Emphasis>"await using"</Emphasis>" syntax."
124+
},
125+
})
126+
.note(markup! {
127+
"Not disposing the object properly can lead some resource or memory leak."
128+
})
129+
)
130+
}
131+
132+
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
133+
let mut mutation = ctx.root().begin();
134+
135+
let decl = ctx
136+
.query()
137+
.parent::<JsVariableDeclaratorList>()?
138+
.parent::<JsVariableDeclaration>()?;
139+
140+
let mut new_decl = decl
141+
.clone()
142+
.with_kind_token(make::token_with_trailing_space(T![using]));
143+
144+
if let DisposableKind::AsyncDisposable = state {
145+
new_decl = new_decl.with_await_token(Some(make::token_with_trailing_space(T![await])));
146+
}
147+
148+
mutation.replace_node(decl, new_decl);
149+
150+
Some(JsRuleAction::new(
151+
ctx.metadata().action_category(ctx.category(), ctx.group()),
152+
ctx.metadata().applicability(),
153+
markup! { "Add the "<Emphasis>"using"</Emphasis>" keyword to dispose the object when leaving the scope." },
154+
mutation,
155+
))
156+
}
157+
}
158+
159+
pub enum DisposableKind {
160+
Disposable,
161+
AsyncDisposable,
162+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/* should generate diagnostics */
2+
const disposable = {
3+
[Symbol.dispose]() {
4+
// do something
5+
}
6+
};
7+
8+
const asyncDisposable = {
9+
async [Symbol.asyncDispose]() {
10+
// do something
11+
}
12+
};
13+
14+
function createDisposable(): Disposable {
15+
return {
16+
[Symbol.dispose]() {
17+
// do something
18+
},
19+
};
20+
}
21+
22+
const createdDisposable = createDisposable();
23+
24+
function createAsyncDisposable(): AsyncDisposable {
25+
return {
26+
async [Symbol.asyncDispose](): Promise<void> {
27+
// do something
28+
},
29+
};
30+
}
31+
32+
const createdAsyncDisposable = createAsyncDisposable();
33+
34+
class DisposableClass implements Disposable {
35+
[Symbol.dispose](): void {
36+
// do something
37+
}
38+
}
39+
40+
const disposableInstance = new DisposableClass();
41+
42+
class AsyncDisposableClass implements AsyncDisposable {
43+
async [Symbol.asyncDispose](): Promise<void> {
44+
// do something
45+
}
46+
}
47+
48+
const asyncDisposableInstance = new AsyncDisposableClass();

0 commit comments

Comments
 (0)