Skip to content

Commit f81e8a1

Browse files
committedAug 31, 2024·
feat(linter): add oxc/no-async-endpoint-handlers (#5364)
Adds `no-async-endpoint-handlers` rules, which bans async functions used as endpoint handlers in Express applications. These do not get caught by Express' error handler, causing the server to crash with an unhandled process rejection error. ```js app.use(async (req, res) => { const foo = await api.getFoo(req.query) // server panics if this function rejects return res.json(foo) }) ``` I could not find this rule implemented in any ESLint plugin, but this is a problem I see quite often and I'm tired of dealing with it. I've added it to `oxc` for now, but we should consider adding an `express` or `api` plugin in the future.
1 parent add1465 commit f81e8a1

File tree

8 files changed

+625
-7
lines changed

8 files changed

+625
-7
lines changed
 

‎crates/oxc_ast/src/ast_impl/js.rs

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use std::{borrow::Cow, cell::Cell, fmt, hash::Hash};
44

55
use oxc_allocator::{Box, FromIn, Vec};
66
use oxc_span::{Atom, GetSpan, SourceType, Span};
7-
use oxc_syntax::{operator::UnaryOperator, reference::ReferenceId, scope::ScopeFlags};
7+
use oxc_syntax::{
8+
operator::UnaryOperator, reference::ReferenceId, scope::ScopeFlags, symbol::SymbolId,
9+
};
810

911
#[cfg(feature = "serialize")]
1012
#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
@@ -1056,6 +1058,14 @@ impl<'a> Function<'a> {
10561058
self.id.as_ref().map(|id| id.name.clone())
10571059
}
10581060

1061+
/// Get the [`SymbolId`] this [`Function`] is bound to.
1062+
///
1063+
/// Returns [`None`] for anonymous functions, or if semantic analysis was skipped.
1064+
#[inline]
1065+
pub fn symbol_id(&self) -> Option<SymbolId> {
1066+
self.id.as_ref().and_then(|id| id.symbol_id.get())
1067+
}
1068+
10591069
pub fn is_typescript_syntax(&self) -> bool {
10601070
matches!(
10611071
self.r#type,

‎crates/oxc_linter/src/rules.rs

+2
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ mod oxc {
390390
pub mod missing_throw;
391391
pub mod no_accumulating_spread;
392392
pub mod no_async_await;
393+
pub mod no_async_endpoint_handlers;
393394
pub mod no_barrel_file;
394395
pub mod no_const_enum;
395396
pub mod no_optional_chaining;
@@ -834,6 +835,7 @@ oxc_macros::declare_all_lint_rules! {
834835
oxc::number_arg_out_of_range,
835836
oxc::only_used_in_recursion,
836837
oxc::no_async_await,
838+
oxc::no_async_endpoint_handlers,
837839
oxc::uninvoked_array_callback,
838840
nextjs::google_font_display,
839841
nextjs::google_font_preconnect,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
use std::ops::Deref;
2+
3+
use oxc_diagnostics::{LabeledSpan, OxcDiagnostic};
4+
use oxc_macros::declare_oxc_lint;
5+
use oxc_span::{CompactStr, Span};
6+
use serde_json::Value;
7+
8+
use crate::{context::LintContext, rule::Rule, utils, AstNode};
9+
use oxc_ast::{
10+
ast::{Argument, ArrowFunctionExpression, Expression, Function},
11+
AstKind,
12+
};
13+
14+
#[derive(Debug, Default, Clone)]
15+
pub struct NoAsyncEndpointHandlers(Box<NoAsyncEndpointHandlersConfig>);
16+
impl Deref for NoAsyncEndpointHandlers {
17+
type Target = NoAsyncEndpointHandlersConfig;
18+
19+
fn deref(&self) -> &Self::Target {
20+
&self.0
21+
}
22+
}
23+
24+
#[derive(Debug, Default, Clone)]
25+
pub struct NoAsyncEndpointHandlersConfig {
26+
allowed_names: Vec<CompactStr>,
27+
}
28+
29+
pub fn no_async_handlers(
30+
function_span: Span,
31+
registered_span: Option<Span>,
32+
name: Option<&str>,
33+
) -> OxcDiagnostic {
34+
#[allow(clippy::cast_possible_truncation)]
35+
const ASYNC_LEN: u32 = "async".len() as u32;
36+
37+
// Only cover "async" in "async function (req, res) {}" or "async (req, res) => {}"
38+
let async_span = Span::sized(function_span.start, ASYNC_LEN);
39+
40+
let labels: &[LabeledSpan] = match (registered_span, name) {
41+
// handler is declared separately from registration
42+
// `async function foo(req, res) {}; app.get('/foo', foo);`
43+
(Some(span), Some(name)) => &[
44+
async_span.label(format!("Async handler '{name}' is declared here")),
45+
span.primary_label("and is registered here"),
46+
],
47+
// Shouldn't happen, since separate declaration/registration requires an
48+
// identifier to be bound
49+
(Some(span), None) => &[
50+
async_span.label("Async handler is declared here"),
51+
span.primary_label("and is registered here"),
52+
],
53+
// `app.get('/foo', async function foo(req, res) {});`
54+
(None, Some(name)) => &[async_span.label(format!("Async handler '{name}' is used here"))],
55+
56+
// `app.get('/foo', async (req, res) => {});`
57+
(None, None) => &[async_span.label("Async handler is used here")],
58+
};
59+
60+
OxcDiagnostic::warn("Express endpoint handlers should not be async.")
61+
.with_labels(labels.iter().cloned())
62+
.with_help("Express <= 4.x does not handle Promise rejections. Use `new Promise((resolve, reject) => { ... }).catch(next)` instead.")
63+
}
64+
65+
declare_oxc_lint!(
66+
/// ### What it does
67+
///
68+
/// Disallows the use of `async` functions as Express endpoint handlers.
69+
///
70+
/// ### Why is this bad?
71+
///
72+
/// Before v5, Express will not automatically handle Promise rejections from
73+
/// handler functions with your application's error handler. You must
74+
/// instead explicitly pass the rejected promise to `next()`.
75+
/// ```js
76+
/// const app = express()
77+
/// app.get('/', (req, res, next) => {
78+
/// new Promise((resolve, reject) => {
79+
/// return User.findById(req.params.id)
80+
/// })
81+
/// .then(user => res.json(user))
82+
/// .catch(next)
83+
/// })
84+
/// ```
85+
///
86+
/// If this is not done, your server will crash with an unhandled promise
87+
/// rejection.
88+
/// ```js
89+
/// const app = express()
90+
/// app.get('/', async (req, res) => {
91+
/// // Server will crash if User.findById rejects
92+
/// const user = await User.findById(req.params.id)
93+
/// res.json(user)
94+
/// })
95+
/// ```
96+
///
97+
/// See [Express' Error Handling
98+
/// Guide](https://expressjs.com/en/guide/error-handling.html) for more
99+
/// information.
100+
///
101+
/// ### Examples
102+
///
103+
/// Examples of **incorrect** code for this rule:
104+
/// ```js
105+
/// const app = express();
106+
/// app.get('/', async (req, res) => {
107+
/// const user = await User.findById(req.params.id);
108+
/// res.json(user);
109+
/// });
110+
///
111+
/// const router = express.Router();
112+
/// router.use(async (req, res, next) => {
113+
/// const user = await User.findById(req.params.id);
114+
/// req.user = user;
115+
/// next();
116+
/// });
117+
///
118+
/// const createUser = async (req, res) => {
119+
/// const user = await User.create(req.body);
120+
/// res.json(user);
121+
/// }
122+
/// app.post('/user', createUser);
123+
///
124+
/// // Async handlers that are imported will not be detected because each
125+
/// // file is checked in isolation. This does not trigger the rule, but still
126+
/// // violates it and _will_ result in server crashes.
127+
/// const asyncHandler = require('./asyncHandler');
128+
/// app.get('/async', asyncHandler);
129+
/// ```
130+
///
131+
/// Examples of **correct** code for this rule:
132+
/// ```js
133+
/// const app = express();
134+
/// // not async
135+
/// app.use((req, res, next) => {
136+
/// req.receivedAt = Date.now();
137+
/// })
138+
///
139+
/// app.get('/', (req, res, next) => {
140+
/// fs.readFile('/file-does-not-exist', (err, data) => {
141+
/// if (err) {
142+
/// next(err) // Pass errors to Express.
143+
/// } else {
144+
/// res.send(data)
145+
/// }
146+
/// })
147+
/// })
148+
///
149+
/// const asyncHandler = async (req, res) => {
150+
/// const user = await User.findById(req.params.id);
151+
/// res.json(user);
152+
/// }
153+
/// app.get('/user', (req, res, next) => asyncHandler(req, res).catch(next))
154+
/// ```
155+
///
156+
/// ## Configuration
157+
///
158+
/// This rule takes the following configuration:
159+
/// ```ts
160+
/// type NoAsyncEndpointHandlersConfig = {
161+
/// /**
162+
/// * An array of names that are allowed to be async.
163+
/// */
164+
/// allowedNames?: string[];
165+
/// }
166+
/// ```
167+
NoAsyncEndpointHandlers,
168+
suspicious
169+
);
170+
171+
impl Rule for NoAsyncEndpointHandlers {
172+
fn from_configuration(value: Value) -> Self {
173+
let mut allowed_names: Vec<CompactStr> = value
174+
.get(0)
175+
.and_then(Value::as_object)
176+
.and_then(|config| config.get("allowedNames"))
177+
.and_then(Value::as_array)
178+
.map(|names| names.iter().filter_map(Value::as_str).map(CompactStr::from).collect())
179+
.unwrap_or_default();
180+
allowed_names.sort_unstable();
181+
allowed_names.dedup();
182+
183+
Self(Box::new(NoAsyncEndpointHandlersConfig { allowed_names }))
184+
}
185+
186+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
187+
let kind = node.kind();
188+
let Some((_endpoint, args)) = utils::as_endpoint_registration(&kind) else {
189+
return;
190+
};
191+
for arg in
192+
args.iter().filter_map(Argument::as_expression).map(Expression::get_inner_expression)
193+
{
194+
self.check_endpoint_arg(ctx, arg);
195+
}
196+
}
197+
}
198+
199+
impl NoAsyncEndpointHandlers {
200+
fn check_endpoint_arg<'a>(&self, ctx: &LintContext<'a>, arg: &Expression<'a>) {
201+
self.check_endpoint_expr(ctx, None, None, arg);
202+
}
203+
204+
fn check_endpoint_expr<'a>(
205+
&self,
206+
ctx: &LintContext<'a>,
207+
id_name: Option<&str>,
208+
registered_at: Option<Span>,
209+
arg: &Expression<'a>,
210+
) {
211+
match arg {
212+
Expression::Identifier(handler) => {
213+
// Unresolved reference? Nothing we can do.
214+
let Some(symbol_id) = handler
215+
.reference_id()
216+
.and_then(|id| ctx.symbols().get_reference(id).symbol_id())
217+
else {
218+
return;
219+
};
220+
221+
// Cannot check imported handlers without cross-file analysis.
222+
let flags = ctx.symbols().get_flags(symbol_id);
223+
if flags.is_import() {
224+
return;
225+
}
226+
227+
let decl_id = ctx.symbols().get_declaration(symbol_id);
228+
let decl_node = ctx.nodes().get_node(decl_id);
229+
let registered_at = registered_at.or(Some(handler.span));
230+
match decl_node.kind() {
231+
AstKind::Function(f) => self.check_function(ctx, registered_at, id_name, f),
232+
AstKind::VariableDeclarator(decl) => {
233+
if let Some(init) = &decl.init {
234+
self.check_endpoint_expr(ctx, id_name, registered_at, init);
235+
}
236+
}
237+
_ => {}
238+
}
239+
}
240+
func if utils::is_endpoint_handler(func) => {
241+
match func {
242+
// `app.get('/', (async?) function (req, res) {}`
243+
Expression::FunctionExpression(f) => {
244+
self.check_function(ctx, registered_at, id_name, f);
245+
}
246+
Expression::ArrowFunctionExpression(f) => {
247+
self.check_arrow(ctx, registered_at, id_name, f);
248+
}
249+
_ => unreachable!(),
250+
}
251+
}
252+
_ => {}
253+
}
254+
}
255+
256+
fn check_function<'a>(
257+
&self,
258+
ctx: &LintContext<'a>,
259+
registered_at: Option<Span>,
260+
id_name: Option<&str>,
261+
f: &Function<'a>,
262+
) {
263+
if !f.r#async {
264+
return;
265+
}
266+
267+
let name = f.name().map(|n| n.as_str()).or(id_name);
268+
if name.is_some_and(|name| self.is_allowed_name(name)) {
269+
return;
270+
}
271+
272+
ctx.diagnostic(no_async_handlers(f.span, registered_at, name));
273+
}
274+
275+
fn check_arrow<'a>(
276+
&self,
277+
ctx: &LintContext<'a>,
278+
registered_at: Option<Span>,
279+
id_name: Option<&str>,
280+
f: &ArrowFunctionExpression<'a>,
281+
) {
282+
if !f.r#async {
283+
return;
284+
}
285+
if id_name.is_some_and(|name| self.is_allowed_name(name)) {
286+
return;
287+
}
288+
289+
ctx.diagnostic(no_async_handlers(f.span, registered_at, id_name));
290+
}
291+
292+
fn is_allowed_name(&self, name: &str) -> bool {
293+
self.allowed_names.binary_search_by(|allowed| allowed.as_str().cmp(name)).is_ok()
294+
}
295+
}
296+
297+
#[test]
298+
fn test() {
299+
use crate::tester::Tester;
300+
use serde_json::json;
301+
302+
let pass = vec![
303+
("app.get('/', fooController)", None),
304+
("app.get('/', (req, res) => {})", None),
305+
("app.get('/', (req, res) => {})", None),
306+
("app.get('/', function (req, res) {})", None),
307+
("app.get('/', middleware, function (req, res) {})", None),
308+
("app.get('/', (req, res, next) => {})", None),
309+
("app.get('/', (err, req, res, next) => {})", None),
310+
("app.get('/', (err, req, res) => {})", None),
311+
("app.get('/', (err, req, res) => {})", None),
312+
("app.get('/', (req, res) => Promise.resolve())", None),
313+
("app.get('/', (req, res) => new Promise((resolve, reject) => resolve()))", None),
314+
("app.use(middleware)", None),
315+
("app.get(middleware)", None),
316+
(
317+
"function ctl(req, res) {}
318+
app.get(ctl)",
319+
None,
320+
),
321+
("weirdName.get('/', async () => {})", None),
322+
("weirdName.get('/', async (notARequestObject) => {})", None),
323+
// allowed names
324+
(
325+
"async function ctl(req, res) {}
326+
app.get(ctl)",
327+
Some(json!([ { "allowedNames": ["ctl"] } ])),
328+
),
329+
(
330+
"
331+
async function middleware(req, res, next) {}
332+
app.use(middleware)
333+
",
334+
Some(json!([ { "allowedNames": ["middleware"] } ])),
335+
),
336+
];
337+
338+
let fail = vec![
339+
("app.get('/', async function (req, res) {})", None),
340+
("app.get('/', async (req, res) => {})", None),
341+
("app.get('/', async (req, res, next) => {})", None),
342+
("weirdName.get('/', async (req, res) => {})", None),
343+
("weirdName.get('/', async (req, res) => {})", None),
344+
(
345+
"
346+
async function foo(req, res) {}
347+
app.post('/', foo)
348+
",
349+
None,
350+
),
351+
(
352+
"
353+
const foo = async (req, res) => {}
354+
app.post('/', foo)
355+
",
356+
None,
357+
),
358+
(
359+
"
360+
async function middleware(req, res, next) {}
361+
app.use(middleware)
362+
",
363+
None,
364+
),
365+
(
366+
"
367+
async function foo(req, res) {}
368+
const bar = foo;
369+
app.post('/', bar)
370+
",
371+
None,
372+
),
373+
];
374+
375+
Tester::new(NoAsyncEndpointHandlers::NAME, pass, fail).test_and_snapshot();
376+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
oxc(no-async-endpoint-handlers): Express endpoint handlers should not be async.
5+
╭─[no_async_endpoint_handlers.tsx:1:14]
6+
1app.get('/', async function (req, res) {})
7+
· ──┬──
8+
· ╰── Async handler is used here
9+
╰────
10+
help: Express <= 4.x does not handle Promise rejections. Use `new Promise((resolve, reject) => { ... }).catch(next)` instead.
11+
12+
oxc(no-async-endpoint-handlers): Express endpoint handlers should not be async.
13+
╭─[no_async_endpoint_handlers.tsx:1:14]
14+
1app.get('/', async (req, res) => {})
15+
· ──┬──
16+
· ╰── Async handler is used here
17+
╰────
18+
help: Express <= 4.x does not handle Promise rejections. Use `new Promise((resolve, reject) => { ... }).catch(next)` instead.
19+
20+
oxc(no-async-endpoint-handlers): Express endpoint handlers should not be async.
21+
╭─[no_async_endpoint_handlers.tsx:1:14]
22+
1app.get('/', async (req, res, next) => {})
23+
· ──┬──
24+
· ╰── Async handler is used here
25+
╰────
26+
help: Express <= 4.x does not handle Promise rejections. Use `new Promise((resolve, reject) => { ... }).catch(next)` instead.
27+
28+
oxc(no-async-endpoint-handlers): Express endpoint handlers should not be async.
29+
╭─[no_async_endpoint_handlers.tsx:1:20]
30+
1weirdName.get('/', async (req, res) => {})
31+
· ──┬──
32+
· ╰── Async handler is used here
33+
╰────
34+
help: Express <= 4.x does not handle Promise rejections. Use `new Promise((resolve, reject) => { ... }).catch(next)` instead.
35+
36+
oxc(no-async-endpoint-handlers): Express endpoint handlers should not be async.
37+
╭─[no_async_endpoint_handlers.tsx:1:20]
38+
1weirdName.get('/', async (req, res) => {})
39+
· ──┬──
40+
· ╰── Async handler is used here
41+
╰────
42+
help: Express <= 4.x does not handle Promise rejections. Use `new Promise((resolve, reject) => { ... }).catch(next)` instead.
43+
44+
oxc(no-async-endpoint-handlers): Express endpoint handlers should not be async.
45+
╭─[no_async_endpoint_handlers.tsx:3:27]
46+
1
47+
2async function foo(req, res) {}
48+
· ──┬──
49+
· ╰── Async handler 'foo' is declared here
50+
3app.post('/', foo)
51+
· ─┬─
52+
· ╰── and is registered here
53+
4
54+
╰────
55+
help: Express <= 4.x does not handle Promise rejections. Use `new Promise((resolve, reject) => { ... }).catch(next)` instead.
56+
57+
oxc(no-async-endpoint-handlers): Express endpoint handlers should not be async.
58+
╭─[no_async_endpoint_handlers.tsx:3:27]
59+
1
60+
2const foo = async (req, res) => {}
61+
· ──┬──
62+
· ╰── Async handler is declared here
63+
3app.post('/', foo)
64+
· ─┬─
65+
· ╰── and is registered here
66+
4
67+
╰────
68+
help: Express <= 4.x does not handle Promise rejections. Use `new Promise((resolve, reject) => { ... }).catch(next)` instead.
69+
70+
oxc(no-async-endpoint-handlers): Express endpoint handlers should not be async.
71+
╭─[no_async_endpoint_handlers.tsx:3:21]
72+
1
73+
2async function middleware(req, res, next) {}
74+
· ──┬──
75+
· ╰── Async handler 'middleware' is declared here
76+
3app.use(middleware)
77+
· ─────┬────
78+
· ╰── and is registered here
79+
4
80+
╰────
81+
help: Express <= 4.x does not handle Promise rejections. Use `new Promise((resolve, reject) => { ... }).catch(next)` instead.
82+
83+
oxc(no-async-endpoint-handlers): Express endpoint handlers should not be async.
84+
╭─[no_async_endpoint_handlers.tsx:4:27]
85+
1
86+
2async function foo(req, res) {}
87+
· ──┬──
88+
· ╰── Async handler 'foo' is declared here
89+
3const bar = foo;
90+
4app.post('/', bar)
91+
· ─┬─
92+
· ╰── and is registered here
93+
5
94+
╰────
95+
help: Express <= 4.x does not handle Promise rejections. Use `new Promise((resolve, reject) => { ... }).catch(next)` instead.
+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use oxc_ast::{
2+
ast::{Argument, Expression, FormalParameter},
3+
AstKind,
4+
};
5+
use oxc_span::Atom;
6+
use phf::{phf_set, set::Set};
7+
8+
/// Check if the given node is registering an endpoint handler or middleware to
9+
/// a route or Express application object. If it is, it
10+
/// returns:
11+
/// - the endpoint path being handled, if found and statically analyzable
12+
/// - the arguments to the handler function, excluding the path (if found)
13+
///
14+
/// ## Example
15+
/// ```js
16+
///
17+
/// app.get('/path', (req, res) => { }); // -> Some(( Some("/path"), [Argument::Expression(Expression::Function(...))] ))
18+
/// app.use(someMiddleware); // -> Some(( None, [Argument::Expression(Expression::IdentifierReference)] ))
19+
///
20+
/// ```
21+
pub fn as_endpoint_registration<'a, 'n>(
22+
node: &'n AstKind<'a>,
23+
) -> Option<(Option<Atom<'a>>, &'n [Argument<'a>])> {
24+
let AstKind::CallExpression(call) = node else {
25+
return None;
26+
};
27+
let callee = call.callee.as_member_expression()?;
28+
let method_name = callee.static_property_name()?;
29+
if !ROUTER_HANDLER_METHOD_NAMES.contains(method_name) {
30+
return None;
31+
}
32+
if call.arguments.is_empty() {
33+
return None;
34+
}
35+
let first = call.arguments[0].as_expression()?;
36+
match first {
37+
Expression::StringLiteral(path) => {
38+
Some((Some(path.value.clone()), &call.arguments.as_slice()[1..]))
39+
}
40+
Expression::TemplateLiteral(template) if template.is_no_substitution_template() => {
41+
Some((template.quasi().clone(), &call.arguments.as_slice()[1..]))
42+
}
43+
_ => Some((None, call.arguments.as_slice())),
44+
}
45+
}
46+
47+
/// Check if the given expression is an endpoint handler function.
48+
///
49+
/// This will yield a lot of false positives if not called on the results of
50+
/// [`as_endpoint_registration`].
51+
#[allow(clippy::similar_names)]
52+
pub fn is_endpoint_handler(maybe_handler: &Expression<'_>) -> bool {
53+
let params = match maybe_handler {
54+
Expression::FunctionExpression(f) => &f.params,
55+
Expression::ArrowFunctionExpression(arrow) => &arrow.params,
56+
_ => return false,
57+
};
58+
59+
// NOTE(@DonIsaac): should we check for destructuring patterns? I don't
60+
// really ever see them used in handlers, and their existence could indicate
61+
// this function is not a handler.
62+
if params.rest.is_some() {
63+
return false;
64+
}
65+
match params.items.as_slice() {
66+
[req] => is_req_param(req),
67+
[req, res] => is_req_param(req) && is_res_param(res),
68+
[req, res, next] => {
69+
is_req_param(req) && is_res_param(res) && is_next_param(next) ||
70+
// (err, req, res)
71+
is_error_param(req) && is_req_param(res) && is_res_param(next)
72+
}
73+
[err, req, res, next] => {
74+
is_error_param(err) && is_req_param(req) && is_res_param(res) && is_next_param(next)
75+
}
76+
_ => false,
77+
}
78+
}
79+
80+
const ROUTER_HANDLER_METHOD_NAMES: Set<&'static str> = phf_set! {
81+
"get",
82+
"post",
83+
"put",
84+
"delete",
85+
"patch",
86+
"options",
87+
"head",
88+
"use",
89+
"all",
90+
};
91+
92+
const COMMON_REQUEST_NAMES: Set<&'static str> = phf_set! {
93+
"r",
94+
"req",
95+
"request",
96+
};
97+
fn is_req_param(param: &FormalParameter) -> bool {
98+
param.pattern.get_identifier().map_or(false, |id| COMMON_REQUEST_NAMES.contains(id.as_str()))
99+
}
100+
101+
const COMMON_RESPONSE_NAMES: Set<&'static str> = phf_set! {
102+
"s",
103+
"res",
104+
"response",
105+
};
106+
fn is_res_param(param: &FormalParameter) -> bool {
107+
param.pattern.get_identifier().map_or(false, |id| COMMON_RESPONSE_NAMES.contains(id.as_str()))
108+
}
109+
110+
const COMMON_NEXT_NAMES: Set<&'static str> = phf_set! {
111+
"n",
112+
"next",
113+
};
114+
fn is_next_param(param: &FormalParameter) -> bool {
115+
param.pattern.get_identifier().map_or(false, |id| COMMON_NEXT_NAMES.contains(id.as_str()))
116+
}
117+
118+
const COMMON_ERROR_NAMES: Set<&'static str> = phf_set! {
119+
"e",
120+
"err",
121+
"error",
122+
"exception",
123+
};
124+
fn is_error_param(param: &FormalParameter) -> bool {
125+
param.pattern.get_identifier().map_or(false, |id| COMMON_ERROR_NAMES.contains(id.as_str()))
126+
}

‎crates/oxc_linter/src/utils/mod.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod config;
2+
mod express;
23
mod jest;
34
mod jsdoc;
45
mod nextjs;
@@ -12,8 +13,8 @@ mod vitest;
1213
use std::{io, path::Path};
1314

1415
pub use self::{
15-
config::*, jest::*, jsdoc::*, nextjs::*, promise::*, react::*, react_perf::*, tree_shaking::*,
16-
unicorn::*, vitest::*,
16+
config::*, express::*, jest::*, jsdoc::*, nextjs::*, promise::*, react::*, react_perf::*,
17+
tree_shaking::*, unicorn::*, vitest::*,
1718
};
1819

1920
/// Check if the Jest rule is adapted to Vitest.

‎crates/oxc_span/src/span/mod.rs

+8
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,18 @@ impl Span {
316316
}
317317

318318
/// Create a [`LabeledSpan`] covering this [`Span`] with the given label.
319+
///
320+
/// Use [`Span::primary_label`] if this is the primary span for the diagnostic.
319321
#[must_use]
320322
pub fn label<S: Into<String>>(self, label: S) -> LabeledSpan {
321323
LabeledSpan::new_with_span(Some(label.into()), self)
322324
}
325+
326+
/// Creates a primary [`LabeledSpan`] covering this [`Span`] with the given label.
327+
#[must_use]
328+
pub fn primary_label<S: Into<String>>(self, label: S) -> LabeledSpan {
329+
LabeledSpan::new_primary_with_span(Some(label.into()), self)
330+
}
323331
}
324332

325333
impl Index<Span> for str {

‎crates/oxc_transformer/src/react/refresh.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,7 @@ fn get_symbol_id_from_function_and_declarator(stmt: &Statement<'_>) -> Vec<Symbo
927927
let mut symbol_ids = vec![];
928928
match stmt {
929929
Statement::FunctionDeclaration(ref func) => {
930-
symbol_ids.push(func.id.as_ref().unwrap().symbol_id.get().unwrap());
930+
symbol_ids.push(func.symbol_id().unwrap());
931931
}
932932
Statement::VariableDeclaration(ref decl) => {
933933
symbol_ids.extend(decl.declarations.iter().filter_map(|decl| {
@@ -936,7 +936,7 @@ fn get_symbol_id_from_function_and_declarator(stmt: &Statement<'_>) -> Vec<Symbo
936936
}
937937
Statement::ExportNamedDeclaration(ref export_decl) => {
938938
if let Some(Declaration::FunctionDeclaration(func)) = &export_decl.declaration {
939-
symbol_ids.push(func.id.as_ref().unwrap().symbol_id.get().unwrap());
939+
symbol_ids.push(func.symbol_id().unwrap());
940940
} else if let Some(Declaration::VariableDeclaration(decl)) = &export_decl.declaration {
941941
symbol_ids.extend(decl.declarations.iter().filter_map(|decl| {
942942
decl.id.get_binding_identifier().and_then(|id| id.symbol_id.get())
@@ -947,8 +947,8 @@ fn get_symbol_id_from_function_and_declarator(stmt: &Statement<'_>) -> Vec<Symbo
947947
if let ExportDefaultDeclarationKind::FunctionDeclaration(func) =
948948
&export_decl.declaration
949949
{
950-
if let Some(id) = func.id.as_ref() {
951-
symbol_ids.push(id.symbol_id.get().unwrap());
950+
if let Some(id) = func.symbol_id() {
951+
symbol_ids.push(id);
952952
}
953953
}
954954
}

0 commit comments

Comments
 (0)
Please sign in to comment.