diff --git a/crates/biome_js_analyze/src/lint/nursery/use_explicit_function_return_type.rs b/crates/biome_js_analyze/src/lint/nursery/use_explicit_function_return_type.rs index 469aa72d34b6..788378c6cd24 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_explicit_function_return_type.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_explicit_function_return_type.rs @@ -4,7 +4,8 @@ use biome_analyze::{ use biome_console::markup; use biome_js_semantic::HasClosureAstNode; use biome_js_syntax::{ - AnyJsBinding, AnyJsExpression, AnyJsFunctionBody, AnyTsType, JsFileSource, JsSyntaxKind, + AnyJsBinding, AnyJsExpression, AnyJsFunctionBody, AnyJsStatement, AnyTsType, JsFileSource, + JsStatementList, JsSyntaxKind, }; use biome_js_syntax::{ AnyJsFunction, JsGetterClassMember, JsGetterObjectMember, JsMethodClassMember, @@ -68,6 +69,40 @@ declare_lint_rule! { /// const func = (value: number) => ({ type: 'X', value }) as any; /// ``` /// + /// The following pattern is considered incorrect code for a higher-order function, as the returned function does not specify a return type: + /// + /// ```ts,expect_diagnostic + /// const arrowFn = () => () => {}; + /// ``` + /// + /// ```ts,expect_diagnostic + /// const arrowFn = () => { + /// return () => { }; + /// } + /// ``` + /// + /// The following pattern is considered incorrect code for a higher-order function because the function body contains multiple statements. We only check whether the first statement is a function return. + /// + /// ```ts,expect_diagnostic + /// // A function has multiple statements in the body + /// function f() { + /// if (x) { + /// return 0; + /// } + /// return (): void => {} + /// } + /// ``` + /// + /// ```ts,expect_diagnostic + /// // A function has multiple statements in the body + /// function f() { + /// let str = "test"; + /// return (): string => { + /// str; + /// } + /// } + /// ``` + /// /// ### Valid /// ```ts /// // No return value should be expected (void) @@ -97,10 +132,14 @@ declare_lint_rule! { /// } /// ``` /// + /// The following patterns are considered correct code for a function immediately returning a value with `as const`: + /// /// ```ts /// const func = (value: number) => ({ foo: 'bar', value }) as const; /// ``` /// + /// The following patterns are considered correct code for a function allowed within specific expression contexts, such as an IIFE, a function passed as an argument, or a function inside an array: + /// /// ```ts /// // Callbacks without return types /// setTimeout(function() { console.log("Hello!"); }, 1000); @@ -110,6 +149,25 @@ declare_lint_rule! { /// (() => {})(); /// ``` /// + /// ```ts + /// // a function inside an array + /// [function () {}, () => {}]; + /// ``` + /// + /// The following pattern is considered correct code for a higher-order function, where the returned function explicitly specifies a return type and the function body contains only one statement: + /// + /// ```ts + /// // the outer function returns an inner function that has a `void` return type + /// const arrowFn = () => (): void => {}; + /// ``` + /// + /// ```ts + /// // the outer function returns an inner function that has a `void` return type + /// const arrowFn = () => { + /// return (): void => { }; + /// } + /// ``` + /// pub UseExplicitFunctionReturnType { version: "next", name: "useExplicitFunctionReturnType", @@ -150,6 +208,10 @@ impl Rule for UseExplicitFunctionReturnType { return None; } + if is_higher_order_function(func) { + return None; + } + let func_range = func.syntax().text_range(); if let Ok(Some(AnyJsBinding::JsIdentifierBinding(id))) = func.id() { return Some(TextRange::new( @@ -266,3 +328,67 @@ fn is_function_used_in_argument_or_expression_list(func: &AnyJsFunction) -> bool ) ) } + +/// Checks whether the given function is a higher-order function, i.e., a function +/// that returns another function either directly in its body or as an expression. +/// +/// # Arguments +/// +/// * `func` - A reference to an `AnyJsFunction` that represents the JavaScript function to inspect. +/// +/// # Returns +/// +/// * `true` if the function returns another function (either a regular function or an arrow function). +/// * `false` if it does not return a function or if the body is not a valid returnable function expression. +/// +/// # Note +/// +/// This function currently **does not support** detecting a return of a function +/// inside other statements, such as `if` statements or `switch` statements. It only checks +/// whether the first statement is a return of a function in a straightforward function body. +fn is_higher_order_function(func: &AnyJsFunction) -> bool { + match func.body().ok() { + Some(AnyJsFunctionBody::AnyJsExpression(expr)) => { + matches!( + expr, + AnyJsExpression::JsArrowFunctionExpression(_) + | AnyJsExpression::JsFunctionExpression(_) + ) + } + Some(AnyJsFunctionBody::JsFunctionBody(func_body)) => { + is_first_statement_function_return(func_body.statements()) + } + _ => false, + } +} + +/// Checks whether the first statement in the given list of JavaScript statements is a return statement +/// that returns a function expression (either a regular function or an arrow function). +/// +/// # Arguments +/// +/// * `statements` - A list of JavaScript statements (`JsStatementList`) to inspect. +/// +/// # Returns +/// +/// * `true` if the list contains a return statement with a function expression as its argument. +/// * `false` if no such return statement is found or if the list is empty. +fn is_first_statement_function_return(statements: JsStatementList) -> bool { + statements + .into_iter() + .next() + .and_then(|stmt| { + if let AnyJsStatement::JsReturnStatement(return_stmt) = stmt { + return_stmt.argument() + } else { + None + } + }) + .map_or(false, |args| { + matches!( + args, + AnyJsExpression::JsFunctionExpression(_) + | AnyJsExpression::JsArrowFunctionExpression(_) + ) + }) +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts index 290a11b5e955..16fe67e58b09 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts @@ -43,4 +43,50 @@ const func = (value: number) => ({ type: 'X', value }) as any; const func = (value: number) => ({ type: 'X', value }) as Action; export default () => {}; -export default function () {} \ No newline at end of file +export default function () {} + +// check higher order functions +const arrowFn = () => () => {}; +const arrowFn = () => function() {} +const arrowFn = () => { + return () => { }; +} + +// does not support detecting a return of a function inside other statements like if, switch, etc. +// we check only the first statment +const arrowFn = (a: number) => { + if (a === 1) { + return (): void => { }; + } else { + return (): number => { + return a + 2 + } + } +} +const arrowFn = (a: number) => { + switch (a) { + case 1: { + return (): void => { }; + } + case 2: { + return (): void => { }; + } + default: { + return (): void => { }; + } + } +} + +function f() { + if (x) { + return 0; + } + return (): void => {} +} + +function fn() { + let str = "hey"; + return function (): string { + return str; + }; +} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts.snap index 3d5794922220..9b84cc810709 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts.snap @@ -50,6 +50,52 @@ const func = (value: number) => ({ type: 'X', value }) as Action; export default () => {}; export default function () {} + +// check higher order functions +const arrowFn = () => () => {}; +const arrowFn = () => function() {} +const arrowFn = () => { + return () => { }; +} + +// does not support detecting a return of a function inside other statements like if, switch, etc. +// we check only the first statment +const arrowFn = (a: number) => { + if (a === 1) { + return (): void => { }; + } else { + return (): number => { + return a + 2 + } + } +} +const arrowFn = (a: number) => { + switch (a) { + case 1: { + return (): void => { }; + } + case 2: { + return (): void => { }; + } + default: { + return (): void => { }; + } + } +} + +function f() { + if (x) { + return 0; + } + return (): void => {} +} + +function fn() { + let str = "hey"; + return function (): string { + return str; + }; +} ``` # Diagnostics @@ -307,6 +353,7 @@ invalid.ts:45:16 lint/nursery/useExplicitFunctionReturnType ━━━━━━ > 45 │ export default () => {}; │ ^^^^^^^^ 46 │ export default function () {} + 47 │ i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. @@ -323,6 +370,156 @@ invalid.ts:46:16 lint/nursery/useExplicitFunctionReturnType ━━━━━━ 45 │ export default () => {}; > 46 │ export default function () {} │ ^^^^^^^^^^^^^^ + 47 │ + 48 │ // check higher order functions + + i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. + + i Add a return type annotation. + + +``` + +``` +invalid.ts:49:23 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 48 │ // check higher order functions + > 49 │ const arrowFn = () => () => {}; + │ ^^^^^^^^ + 50 │ const arrowFn = () => function() {} + 51 │ const arrowFn = () => { + + i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. + + i Add a return type annotation. + + +``` + +``` +invalid.ts:50:23 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 48 │ // check higher order functions + 49 │ const arrowFn = () => () => {}; + > 50 │ const arrowFn = () => function() {} + │ ^^^^^^^^^^^^^ + 51 │ const arrowFn = () => { + 52 │ return () => { }; + + i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. + + i Add a return type annotation. + + +``` + +``` +invalid.ts:52:10 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 50 │ const arrowFn = () => function() {} + 51 │ const arrowFn = () => { + > 52 │ return () => { }; + │ ^^^^^^^^^ + 53 │ } + 54 │ + + i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. + + i Add a return type annotation. + + +``` + +``` +invalid.ts:57:17 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 55 │ // does not support detecting a return of a function inside other statements like if, switch, etc. + 56 │ // we check only the first statment· + > 57 │ const arrowFn = (a: number) => { + │ ^^^^^^^^^^^^^^^^ + > 58 │ if (a === 1) { + ... + > 64 │ } + > 65 │ } + │ ^ + 66 │ const arrowFn = (a: number) => { + 67 │ switch (a) { + + i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. + + i Add a return type annotation. + + +``` + +``` +invalid.ts:66:17 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 64 │ } + 65 │ } + > 66 │ const arrowFn = (a: number) => { + │ ^^^^^^^^^^^^^^^^ + > 67 │ switch (a) { + ... + > 77 │ } + > 78 │ } + │ ^ + 79 │ + 80 │ function f() { + + i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. + + i Add a return type annotation. + + +``` + +``` +invalid.ts:78:2 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 76 │ } + 77 │ } + > 78 │ } + │ + > 79 │ + > 80 │ function f() { + │ ^^^^^^^^^^ + 81 │ if (x) { + 82 │ return 0; + + i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. + + i Add a return type annotation. + + +``` + +``` +invalid.ts:85:2 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 83 │ } + 84 │ return (): void => {} + > 85 │ } + │ + > 86 │ + > 87 │ function fn() { + │ ^^^^^^^^^^^ + 88 │ let str = "hey"; + 89 │ return function (): string { i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts index aa6e78b37dc6..0c381b08a967 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts @@ -54,4 +54,12 @@ fn(function () {}); (() => { console.log("This is an IIFE"); })(); -setTimeout(function() { console.log("Hello!"); }, 1000); \ No newline at end of file +setTimeout(function() { console.log("Hello!"); }, 1000); + + +// check higher order functions +const arrowFn = () => (): void => {}; +const arrowFn = () => function(): void {} +const arrowFn = () => { + return (): void => { }; +} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts.snap index 23bdedafbf97..80b8221a7297 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts.snap @@ -61,4 +61,12 @@ fn(function () {}); console.log("This is an IIFE"); })(); setTimeout(function() { console.log("Hello!"); }, 1000); + + +// check higher order functions +const arrowFn = () => (): void => {}; +const arrowFn = () => function(): void {} +const arrowFn = () => { + return (): void => { }; +} ```