Skip to content

Commit 5d4bf12

Browse files
jorgezreikljharb
authored andcommitted
[New] async-server-action: Add rule to require that server actions be async
1 parent f71d562 commit 5d4bf12

File tree

6 files changed

+1070
-0
lines changed

6 files changed

+1070
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
77
## Unreleased
88

99
### Added
10+
* [`async-server-action`]: add rule ([#3729][] @jorgezreik)
1011
* [`jsx-props-no-multi-spaces`]: improve autofix for multi-line ([#3930][] @justisb)
1112
* [`jsx-handler-names`]: support namespaced component names ([#3943][] @takuji)
1213
* [`jsx-no-leaked-render`]: add `ignoreAttributes` option ([#3441][] @aleclarson)
@@ -34,6 +35,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
3435
[#3928]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3928
3536
[#3923]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3923
3637
[#3441]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3441
38+
[#3729]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3729
3739

3840
## [7.37.5] - 2025.04.03
3941

@@ -265,6 +267,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
265267
* [`no-unknown-property`]: support `popover`, `popovertarget`, `popovertargetaction` attributes ([#3707][] @ljharb)
266268
* [`no-unknown-property`]: only match `data-*` attributes containing `-` ([#3713][] @silverwind)
267269
* [`checked-requires-onchange-or-readonly`]: correct options that were behaving opposite ([#3715][] @jaesoekjjang)
270+
* [`boolean-prop-naming`]: avoid a crash with a non-TSTypeReference type ([#3718][] @developer-bandi)
268271

269272
### Changed
270273
* [`boolean-prop-naming`]: improve error message (@ljharb)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ module.exports = [
291291

292292
| Name                                  | Description | 💼 | 🚫 | 🔧 | 💡 ||
293293
| :------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- | :- | :- |
294+
| [async-server-action](docs/rules/async-server-action.md) | Require functions with the `use server` directive to be async | | | | 💡 | |
294295
| [boolean-prop-naming](docs/rules/boolean-prop-naming.md) | Enforces consistent naming for boolean props | | | | | |
295296
| [button-has-type](docs/rules/button-has-type.md) | Disallow usage of `button` elements without an explicit `type` attribute | | | | | |
296297
| [checked-requires-onchange-or-readonly](docs/rules/checked-requires-onchange-or-readonly.md) | Enforce using `onChange` or `readonly` attribute when `checked` is used | | | | | |

docs/rules/async-server-action.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# react/async-server-action
2+
3+
📝 Require functions with the `use server` directive to be async.
4+
5+
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
6+
7+
<!-- end auto-generated rule header -->
8+
9+
Require Server Actions (functions with the `use server` directive) to be async, as mandated by the `use server` [spec](https://react.dev/reference/react/use-server).
10+
11+
This must be the case even if the function does not use `await` or `return` a promise.
12+
13+
## Rule Details
14+
15+
Examples of **incorrect** code for this rule:
16+
17+
```jsx
18+
<form
19+
action={() => {
20+
'use server';
21+
...
22+
}}
23+
>
24+
...
25+
</form>
26+
```
27+
28+
```jsx
29+
function action() {
30+
'use server';
31+
...
32+
}
33+
```
34+
35+
Examples of **correct** code for this rule:
36+
37+
```jsx
38+
<form
39+
action={async () => {
40+
'use server';
41+
...
42+
}}
43+
>
44+
...
45+
</form>
46+
```
47+
48+
```jsx
49+
async function action() {
50+
'use server';
51+
...
52+
}
53+
```
54+
55+
## When Not To Use It
56+
57+
If you are not using React Server Components.

lib/rules/async-server-action.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @fileoverview Require functions with the `use server` directive to be async
3+
* @author Jorge Zreik
4+
*/
5+
6+
'use strict';
7+
8+
const docsUrl = require('../util/docsUrl');
9+
const report = require('../util/report');
10+
11+
// ------------------------------------------------------------------------------
12+
// Rule Definition
13+
// ------------------------------------------------------------------------------
14+
15+
const messages = {
16+
asyncServerAction: 'Server Actions must be async',
17+
suggestAsyncNamed: 'Make {{functionName}} an `async` function',
18+
suggestAsyncAnon: 'Make this function `async`',
19+
};
20+
21+
/** @type {import('eslint').Rule.RuleModule} */
22+
module.exports = {
23+
meta: {
24+
docs: {
25+
description: 'Require functions with the `use server` directive to be async',
26+
category: 'Possible Errors',
27+
recommended: false,
28+
url: docsUrl('async-server-action'),
29+
},
30+
31+
messages,
32+
33+
type: 'suggestion',
34+
hasSuggestions: true,
35+
36+
schema: [],
37+
},
38+
39+
create(context) {
40+
return {
41+
':function[async=false][generator=false]>BlockStatement>:first-child[expression.value="use server"]'(node) {
42+
const currentFunction = node.parent.parent;
43+
const parent = currentFunction.parent;
44+
const isMethod = parent.type === 'MethodDefinition' || (parent.type === 'Property' && parent.method);
45+
46+
let name;
47+
if (currentFunction.id) {
48+
name = currentFunction.id.name;
49+
} else if (isMethod && parent.key && parent.key.type === 'Identifier') {
50+
name = parent.key.name;
51+
}
52+
53+
const suggestMessage = name ? messages.suggestAsyncNamed : messages.suggestAsyncAnon;
54+
const data = name ? { functionName: `\`${name}\`` } : {};
55+
report(context, messages.asyncServerAction, 'asyncServerAction', {
56+
node: currentFunction,
57+
data,
58+
suggest: [{
59+
desc: suggestMessage,
60+
data,
61+
fix(fixer) {
62+
if (isMethod) {
63+
return fixer.insertTextBefore(parent.key, 'async ');
64+
}
65+
return fixer.insertTextBefore(currentFunction, 'async ');
66+
},
67+
}],
68+
});
69+
},
70+
};
71+
},
72+
};

lib/rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
/** @satisfies {Record<string, import('eslint').Rule.RuleModule>} */
66
const rules = {
7+
'async-server-action': require('./async-server-action'),
78
'boolean-prop-naming': require('./boolean-prop-naming'),
89
'button-has-type': require('./button-has-type'),
910
'checked-requires-onchange-or-readonly': require('./checked-requires-onchange-or-readonly'),

0 commit comments

Comments
 (0)