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

Added the nursery rule [`useAwaitThenable`](https://biomejs.dev/linter/rules/use-await-thenable/), which enforces that `await` is only used on Promise values.

#### Invalid

```js
await 'value';

const createValue = () => 'value';
await createValue();
```

#### Caution

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.
16 changes: 16 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

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

121 changes: 71 additions & 50 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

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

7 changes: 4 additions & 3 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ define_categories! {
"lint/nursery/noContinue": "https://biomejs.dev/linter/rules/no-continue",
"lint/nursery/noDeprecatedImports": "https://biomejs.dev/linter/rules/no-deprecated-imports",
"lint/nursery/noDuplicateDependencies": "https://biomejs.dev/linter/rules/no-duplicate-dependencies",
"lint/nursery/noDuplicatedSpreadProps": "https://biomejs.dev/linter/rules/no-duplicated-spread-props",
"lint/nursery/noEmptySource": "https://biomejs.dev/linter/rules/no-empty-source",
"lint/nursery/noEqualsToNull": "https://biomejs.dev/linter/rules/no-equals-to-null",
"lint/nursery/noFloatingPromises": "https://biomejs.dev/linter/rules/no-floating-promises",
Expand All @@ -185,7 +186,6 @@ define_categories! {
"lint/nursery/noProto": "https://biomejs.dev/linter/rules/no-proto",
"lint/nursery/noReactForwardRef": "https://biomejs.dev/linter/rules/no-react-forward-ref",
"lint/nursery/noShadow": "https://biomejs.dev/linter/rules/no-shadow",
"lint/nursery/noDuplicatedSpreadProps": "https://biomejs.dev/linter/rules/no-duplicated-spread-props",
"lint/nursery/noSyncScripts": "https://biomejs.dev/linter/rules/no-sync-scripts",
"lint/nursery/noTernary": "https://biomejs.dev/linter/rules/no-ternary",
"lint/nursery/noUnknownAttribute": "https://biomejs.dev/linter/rules/no-unknown-attribute",
Expand All @@ -200,10 +200,11 @@ define_categories! {
"lint/nursery/noVueDuplicateKeys": "https://biomejs.dev/linter/rules/no-vue-duplicate-keys",
"lint/nursery/noVueReservedKeys": "https://biomejs.dev/linter/rules/no-vue-reserved-keys",
"lint/nursery/noVueReservedProps": "https://biomejs.dev/linter/rules/no-vue-reserved-props",
"lint/nursery/noVueVIfWithVFor": "https://biomejs.dev/linter/rules/no-vue-v-if-with-v-for",
"lint/nursery/noVueSetupPropsReactivityLoss": "https://biomejs.dev/linter/rules/no-vue-setup-props-reactivity-loss",
"lint/nursery/noVueVIfWithVFor": "https://biomejs.dev/linter/rules/no-vue-v-if-with-v-for",
"lint/nursery/useAnchorHref": "https://biomejs.dev/linter/rules/use-anchor-href",
"lint/nursery/useArraySortCompare": "https://biomejs.dev/linter/rules/use-array-sort-compare",
"lint/nursery/useAwaitThenable": "https://biomejs.dev/linter/rules/use-await-thenable",
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
"lint/nursery/useConsistentArrowReturn": "https://biomejs.dev/linter/rules/use-consistent-arrow-return",
"lint/nursery/useConsistentGraphqlDescriptions": "https://biomejs.dev/linter/rules/use-consistent-graphql-descriptions",
Expand All @@ -218,8 +219,8 @@ define_categories! {
"lint/nursery/useMaxParams": "https://biomejs.dev/linter/rules/use-max-params",
"lint/nursery/useQwikMethodUsage": "https://biomejs.dev/linter/rules/use-qwik-method-usage",
"lint/nursery/useQwikValidLexicalScope": "https://biomejs.dev/linter/rules/use-qwik-valid-lexical-scope",
"lint/nursery/useRequiredScripts": "https://biomejs.dev/linter/rules/use-required-scripts",
"lint/nursery/useRegexpExec": "https://biomejs.dev/linter/rules/use-regexp-exec",
"lint/nursery/useRequiredScripts": "https://biomejs.dev/linter/rules/use-required-scripts",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
"lint/nursery/useSpread": "https://biomejs.dev/linter/rules/no-spread",
"lint/nursery/useUniqueGraphqlOperationName": "https://biomejs.dev/linter/rules/use-unique-graphql-operation-name",
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub mod no_vue_reserved_keys;
pub mod no_vue_reserved_props;
pub mod no_vue_setup_props_reactivity_loss;
pub mod use_array_sort_compare;
pub mod use_await_thenable;
pub mod use_consistent_arrow_return;
pub mod use_exhaustive_switch_cases;
pub mod use_explicit_type;
Expand All @@ -47,4 +48,4 @@ pub mod use_sorted_classes;
pub mod use_spread;
pub mod use_vue_define_macros_order;
pub mod use_vue_multi_word_component_names;
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 ,] } }
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 ,] } }
88 changes: 88 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/use_await_thenable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use biome_analyze::{
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_js_syntax::JsAwaitExpression;
use biome_rowan::AstNode;
use biome_rule_options::use_await_thenable::UseAwaitThenableOptions;

use crate::services::typed::Typed;

declare_lint_rule! {
/// Enforce that `await` is _only_ used on `Promise` values.
///
/// :::caution
/// At the moment, this rule only checks for instances of the global
/// `Promise` class. This is a major shortcoming compared to the ESLint
/// rule if you are using custom `Promise`-like implementations such as
/// [Bluebird](http://bluebirdjs.com/) or in-house solutions.
/// :::
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic,file=invalid-primitive.js
/// await 'value';
/// ```
///
/// ```js,expect_diagnostic,file=invalid-function-call.js
/// const createValue = () => 'value';
/// await createValue();
/// ```
///
/// ### Valid
///
/// ```js,file=valid-examples.js
/// await Promise.resolve('value');
///
/// const createValue = async () => 'value';
/// await createValue();
/// ```
///
pub UseAwaitThenable {
version: "next",
name: "useAwaitThenable",
language: "js",
recommended: false,
sources: &[RuleSource::EslintTypeScript("use-await-thenable").inspired()],
domains: &[RuleDomain::Project],
}
}

impl Rule for UseAwaitThenable {
type Query = Typed<JsAwaitExpression>;
type State = ();
type Signals = Option<Self::State>;
type Options = UseAwaitThenableOptions;

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

// Uncomment the following line for debugging convenience:
//let printed = format!("type of {expression:?} = {ty:?}");

(ty.is_inferred() && !ty.is_promise_instance()).then_some(())
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
"`await` used on a non-Promise value."
},
)
.note(markup! {
"This may happen if you accidentally used `await` on a synchronous value."
})
.note(markup! {
"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"
}),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* should generate diagnostics */

await 'value';

const createValue = () => 'value';
await createValue();
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid.js
---
# Input
```js
/* should generate diagnostics */

await 'value';

const createValue = () => 'value';
await createValue();

```

# Diagnostics
```
invalid.js:3:1 lint/nursery/useAwaitThenable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i `await` used on a non-Promise value.

1 │ /* should generate diagnostics */
2 │
> 3 │ await 'value';
│ ^^^^^^^^^^^^^
4 │
5 │ const createValue = () => 'value';

i This may happen if you accidentally used `await` on a synchronous value.

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


```

```
invalid.js:6:1 lint/nursery/useAwaitThenable ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i `await` used on a non-Promise value.

5 │ const createValue = () => 'value';
> 6 │ await createValue();
│ ^^^^^^^^^^^^^^^^^^^
7 │

i This may happen if you accidentally used `await` on a synchronous value.

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


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* should not generate diagnostics */

await Promise.resolve('value');

const createValue = async () => 'value';
await createValue();
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: valid.js
---
# Input
```js
/* should not generate diagnostics */
await Promise.resolve('value');
const createValue = async () => 'value';
await createValue();
```
1 change: 1 addition & 0 deletions crates/biome_rule_options/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ pub mod use_arrow_function;
pub mod use_as_const_assertion;
pub mod use_at_index;
pub mod use_await;
pub mod use_await_thenable;
pub mod use_biome_ignore_folder;
pub mod use_block_statements;
pub mod use_button_type;
Expand Down
6 changes: 6 additions & 0 deletions crates/biome_rule_options/src/use_await_thenable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use biome_deserialize_macros::{Deserializable, Merge};
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct UseAwaitThenableOptions {}
20 changes: 17 additions & 3 deletions packages/@biomejs/backend-jsonrpc/src/workspace.ts

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

Loading
Loading