Skip to content

Commit 343dc4d

Browse files
arendjrautofix-ci[bot]ematipico
authored
feat(linter): implement useAwaitThenable (#8341)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
1 parent c7915c4 commit 343dc4d

16 files changed

Lines changed: 329 additions & 59 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`useAwaitThenable`](https://biomejs.dev/linter/rules/use-await-thenable/), which enforces that `await` is only used on Promise values.
6+
7+
#### Invalid
8+
9+
```js
10+
await 'value';
11+
12+
const createValue = () => 'value';
13+
await createValue();
14+
```
15+
16+
#### Caution
17+
18+
This is a first iteration of the rule, and does not yet detect generic ["thenable"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables) values.

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

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

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

Lines changed: 71 additions & 50 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: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ define_categories! {
168168
"lint/nursery/noContinue": "https://biomejs.dev/linter/rules/no-continue",
169169
"lint/nursery/noDeprecatedImports": "https://biomejs.dev/linter/rules/no-deprecated-imports",
170170
"lint/nursery/noDuplicateDependencies": "https://biomejs.dev/linter/rules/no-duplicate-dependencies",
171+
"lint/nursery/noDuplicatedSpreadProps": "https://biomejs.dev/linter/rules/no-duplicated-spread-props",
171172
"lint/nursery/noEmptySource": "https://biomejs.dev/linter/rules/no-empty-source",
172173
"lint/nursery/noEqualsToNull": "https://biomejs.dev/linter/rules/no-equals-to-null",
173174
"lint/nursery/noFloatingPromises": "https://biomejs.dev/linter/rules/no-floating-promises",
@@ -185,7 +186,6 @@ define_categories! {
185186
"lint/nursery/noProto": "https://biomejs.dev/linter/rules/no-proto",
186187
"lint/nursery/noReactForwardRef": "https://biomejs.dev/linter/rules/no-react-forward-ref",
187188
"lint/nursery/noShadow": "https://biomejs.dev/linter/rules/no-shadow",
188-
"lint/nursery/noDuplicatedSpreadProps": "https://biomejs.dev/linter/rules/no-duplicated-spread-props",
189189
"lint/nursery/noSyncScripts": "https://biomejs.dev/linter/rules/no-sync-scripts",
190190
"lint/nursery/noTernary": "https://biomejs.dev/linter/rules/no-ternary",
191191
"lint/nursery/noUnknownAttribute": "https://biomejs.dev/linter/rules/no-unknown-attribute",
@@ -200,10 +200,11 @@ define_categories! {
200200
"lint/nursery/noVueDuplicateKeys": "https://biomejs.dev/linter/rules/no-vue-duplicate-keys",
201201
"lint/nursery/noVueReservedKeys": "https://biomejs.dev/linter/rules/no-vue-reserved-keys",
202202
"lint/nursery/noVueReservedProps": "https://biomejs.dev/linter/rules/no-vue-reserved-props",
203-
"lint/nursery/noVueVIfWithVFor": "https://biomejs.dev/linter/rules/no-vue-v-if-with-v-for",
204203
"lint/nursery/noVueSetupPropsReactivityLoss": "https://biomejs.dev/linter/rules/no-vue-setup-props-reactivity-loss",
204+
"lint/nursery/noVueVIfWithVFor": "https://biomejs.dev/linter/rules/no-vue-v-if-with-v-for",
205205
"lint/nursery/useAnchorHref": "https://biomejs.dev/linter/rules/use-anchor-href",
206206
"lint/nursery/useArraySortCompare": "https://biomejs.dev/linter/rules/use-array-sort-compare",
207+
"lint/nursery/useAwaitThenable": "https://biomejs.dev/linter/rules/use-await-thenable",
207208
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
208209
"lint/nursery/useConsistentArrowReturn": "https://biomejs.dev/linter/rules/use-consistent-arrow-return",
209210
"lint/nursery/useConsistentGraphqlDescriptions": "https://biomejs.dev/linter/rules/use-consistent-graphql-descriptions",
@@ -218,8 +219,8 @@ define_categories! {
218219
"lint/nursery/useMaxParams": "https://biomejs.dev/linter/rules/use-max-params",
219220
"lint/nursery/useQwikMethodUsage": "https://biomejs.dev/linter/rules/use-qwik-method-usage",
220221
"lint/nursery/useQwikValidLexicalScope": "https://biomejs.dev/linter/rules/use-qwik-valid-lexical-scope",
221-
"lint/nursery/useRequiredScripts": "https://biomejs.dev/linter/rules/use-required-scripts",
222222
"lint/nursery/useRegexpExec": "https://biomejs.dev/linter/rules/use-regexp-exec",
223+
"lint/nursery/useRequiredScripts": "https://biomejs.dev/linter/rules/use-required-scripts",
223224
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
224225
"lint/nursery/useSpread": "https://biomejs.dev/linter/rules/no-spread",
225226
"lint/nursery/useUniqueGraphqlOperationName": "https://biomejs.dev/linter/rules/use-unique-graphql-operation-name",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub mod no_vue_reserved_keys;
3535
pub mod no_vue_reserved_props;
3636
pub mod no_vue_setup_props_reactivity_loss;
3737
pub mod use_array_sort_compare;
38+
pub mod use_await_thenable;
3839
pub mod use_consistent_arrow_return;
3940
pub mod use_exhaustive_switch_cases;
4041
pub mod use_explicit_type;
@@ -47,4 +48,4 @@ pub mod use_sorted_classes;
4748
pub mod use_spread;
4849
pub mod use_vue_define_macros_order;
4950
pub mod use_vue_multi_word_component_names;
50-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [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_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , 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_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , 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_consistent_arrow_return :: UseConsistentArrowReturn , 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_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
51+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [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_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , 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_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , 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_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_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use biome_analyze::{
2+
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_js_syntax::JsAwaitExpression;
6+
use biome_rowan::AstNode;
7+
use biome_rule_options::use_await_thenable::UseAwaitThenableOptions;
8+
9+
use crate::services::typed::Typed;
10+
11+
declare_lint_rule! {
12+
/// Enforce that `await` is _only_ used on `Promise` values.
13+
///
14+
/// :::caution
15+
/// At the moment, this rule only checks for instances of the global
16+
/// `Promise` class. This is a major shortcoming compared to the ESLint
17+
/// rule if you are using custom `Promise`-like implementations such as
18+
/// [Bluebird](http://bluebirdjs.com/) or in-house solutions.
19+
/// :::
20+
///
21+
/// ## Examples
22+
///
23+
/// ### Invalid
24+
///
25+
/// ```js,expect_diagnostic,file=invalid-primitive.js
26+
/// await 'value';
27+
/// ```
28+
///
29+
/// ```js,expect_diagnostic,file=invalid-function-call.js
30+
/// const createValue = () => 'value';
31+
/// await createValue();
32+
/// ```
33+
///
34+
/// ### Valid
35+
///
36+
/// ```js,file=valid-examples.js
37+
/// await Promise.resolve('value');
38+
///
39+
/// const createValue = async () => 'value';
40+
/// await createValue();
41+
/// ```
42+
///
43+
pub UseAwaitThenable {
44+
version: "next",
45+
name: "useAwaitThenable",
46+
language: "js",
47+
recommended: false,
48+
sources: &[RuleSource::EslintTypeScript("use-await-thenable").inspired()],
49+
domains: &[RuleDomain::Project],
50+
}
51+
}
52+
53+
impl Rule for UseAwaitThenable {
54+
type Query = Typed<JsAwaitExpression>;
55+
type State = ();
56+
type Signals = Option<Self::State>;
57+
type Options = UseAwaitThenableOptions;
58+
59+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
60+
let node = ctx.query();
61+
let expression = node.argument().ok()?;
62+
let ty = ctx.type_of_expression(&expression);
63+
64+
// Uncomment the following line for debugging convenience:
65+
//let printed = format!("type of {expression:?} = {ty:?}");
66+
67+
(ty.is_inferred() && !ty.is_promise_instance()).then_some(())
68+
}
69+
70+
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
71+
let node = ctx.query();
72+
Some(
73+
RuleDiagnostic::new(
74+
rule_category!(),
75+
node.range(),
76+
markup! {
77+
"`await` used on a non-Promise value."
78+
},
79+
)
80+
.note(markup! {
81+
"This may happen if you accidentally used `await` on a synchronous value."
82+
})
83+
.note(markup! {
84+
"Please ensure the value is not a custom \"thenable\" implementation before removing the `await`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables"
85+
}),
86+
)
87+
}
88+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* should generate diagnostics */
2+
3+
await 'value';
4+
5+
const createValue = () => 'value';
6+
await createValue();
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.js
4+
---
5+
# Input
6+
```js
7+
/* should generate diagnostics */
8+
9+
await 'value';
10+
11+
const createValue = () => 'value';
12+
await createValue();
13+
14+
```
15+
16+
# Diagnostics
17+
```
18+
invalid.js:3:1 lint/nursery/useAwaitThenable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19+
20+
i `await` used on a non-Promise value.
21+
22+
1 │ /* should generate diagnostics */
23+
2 │
24+
> 3 │ await 'value';
25+
│ ^^^^^^^^^^^^^
26+
4 │
27+
5 │ const createValue = () => 'value';
28+
29+
i This may happen if you accidentally used `await` on a synchronous value.
30+
31+
i Please ensure the value is not a custom "thenable" implementation before removing the `await`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
32+
33+
34+
```
35+
36+
```
37+
invalid.js:6:1 lint/nursery/useAwaitThenable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
38+
39+
i `await` used on a non-Promise value.
40+
41+
5 │ const createValue = () => 'value';
42+
> 6 │ await createValue();
43+
│ ^^^^^^^^^^^^^^^^^^^
44+
7 │
45+
46+
i This may happen if you accidentally used `await` on a synchronous value.
47+
48+
i Please ensure the value is not a custom "thenable" implementation before removing the `await`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
49+
50+
51+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/* should not generate diagnostics */
2+
3+
await Promise.resolve('value');
4+
5+
const createValue = async () => 'value';
6+
await createValue();

0 commit comments

Comments
 (0)