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 3cd1520f13cd..46000228b2e5 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,14 +4,16 @@ use biome_analyze::{ use biome_console::markup; use biome_js_semantic::HasClosureAstNode; use biome_js_syntax::{ - AnyJsBinding, AnyJsExpression, AnyJsFunctionBody, AnyJsStatement, AnyTsType, JsFileSource, - JsStatementList, JsSyntaxKind, + AnyJsBinding, AnyJsExpression, AnyJsFunctionBody, AnyJsStatement, AnyTsType, JsCallExpression, + JsFileSource, JsFormalParameter, JsInitializerClause, JsLanguage, JsObjectExpression, + JsParenthesizedExpression, JsPropertyClassMember, JsPropertyObjectMember, JsStatementList, + JsSyntaxKind, JsVariableDeclarator, }; use biome_js_syntax::{ AnyJsFunction, JsGetterClassMember, JsGetterObjectMember, JsMethodClassMember, JsMethodObjectMember, }; -use biome_rowan::{declare_node_union, AstNode, SyntaxNodeOptionExt, TextRange}; +use biome_rowan::{declare_node_union, AstNode, SyntaxNode, SyntaxNodeOptionExt, TextRange}; declare_lint_rule! { /// Require explicit return types on functions and class methods. @@ -168,6 +170,38 @@ declare_lint_rule! { /// } /// ``` /// + /// The following patterns are considered correct for type annotations on variables in function expressions: + /// + /// ```ts + /// // A function with a type assertion using `as` + /// const asTyped = (() => '') as () => string; + /// ``` + /// + /// ```ts + /// // A function with a type assertion using `<>` + /// const castTyped = <() => string>(() => ''); + /// ``` + /// + /// ```ts + /// // A variable declarator with a type annotation. + /// type FuncType = () => string; + /// const arrowFn: FuncType = () => 'test'; + /// ``` + /// + /// ```ts + /// // A function is a default parameter with a type annotation + /// type CallBack = () => void; + /// const f = (gotcha: CallBack = () => { }): void => { }; + /// ``` + /// + /// ```ts + /// // A class property with a type annotation + /// type MethodType = () => void; + /// class App { + /// private method: MethodType = () => { }; + /// } + /// ``` + /// pub UseExplicitFunctionReturnType { version: "1.9.3", name: "useExplicitFunctionReturnType", @@ -204,7 +238,11 @@ impl Rule for UseExplicitFunctionReturnType { return None; } - if is_function_used_in_argument_or_expression_list(func) { + if is_iife(func) { + return None; + } + + if is_function_used_in_argument_or_array(func) { return None; } @@ -212,6 +250,10 @@ impl Rule for UseExplicitFunctionReturnType { return None; } + if is_typed_function_expressions(func) { + return None; + } + let func_range = func.syntax().text_range(); if let Ok(Some(AnyJsBinding::JsIdentifierBinding(id))) = func.id() { return Some(TextRange::new( @@ -313,22 +355,27 @@ fn is_direct_const_assertion_in_arrow_functions(func: &AnyJsFunction) -> bool { /// JS_ARRAY_ELEMENT_LIST: /// - `[function () {}, () => {}];` /// -/// JS_PARENTHESIZED_EXPRESSION: -/// - `(function () {});` -/// - `(() => {})();` -fn is_function_used_in_argument_or_expression_list(func: &AnyJsFunction) -> bool { +fn is_function_used_in_argument_or_array(func: &AnyJsFunction) -> bool { matches!( func.syntax().parent().kind(), - Some( - JsSyntaxKind::JS_CALL_ARGUMENT_LIST - | JsSyntaxKind::JS_ARRAY_ELEMENT_LIST - // We include JS_PARENTHESIZED_EXPRESSION for IIFE (Immediately Invoked Function Expressions). - // We also assume that the parent of the parent is a call expression. - | JsSyntaxKind::JS_PARENTHESIZED_EXPRESSION - ) + Some(JsSyntaxKind::JS_CALL_ARGUMENT_LIST | JsSyntaxKind::JS_ARRAY_ELEMENT_LIST) ) } +/// Checks if a function is an IIFE (Immediately Invoked Function Expressions) +/// +/// # Examples +/// +/// ```typescript +/// (function () {}); +/// (() => {})(); +/// ``` +fn is_iife(func: &AnyJsFunction) -> bool { + func.parent::() + .and_then(|expr| expr.parent::()) + .is_some() +} + /// 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. /// @@ -384,7 +431,7 @@ fn is_first_statement_function_return(statements: JsStatementList) -> bool { None } }) - .map_or(false, |args| { + .is_some_and(|args| { matches!( args, AnyJsExpression::JsFunctionExpression(_) @@ -392,3 +439,114 @@ fn is_first_statement_function_return(statements: JsStatementList) -> bool { ) }) } + +/// Checks if a given function expression has a type annotation. +fn is_typed_function_expressions(func: &AnyJsFunction) -> bool { + let syntax = func.syntax(); + is_type_assertion(syntax) + || is_variable_declarator_with_type_annotation(syntax) + || is_default_function_parameter_with_type_annotation(syntax) + || is_class_property_with_type_annotation(syntax) + || is_property_of_object_with_type(syntax) +} + +/// Checks if a function is a variable declarator with a type annotation. +/// +/// # Examples +/// +/// ```typescript +/// type FuncType = () => string; +/// const arrowFn: FuncType = () => 'test'; +/// ``` +fn is_variable_declarator_with_type_annotation(syntax: &SyntaxNode) -> bool { + syntax + .parent() + .and_then(JsInitializerClause::cast) + .and_then(|init| init.parent::()) + .is_some_and(|decl| decl.variable_annotation().is_some()) +} + +/// Checks if a function is a default parameter with a type annotation. +/// +/// # Examples +/// +/// ```typescript +/// type CallBack = () => void; +/// const f = (gotcha: CallBack = () => { }): void => { }; +/// ``` +fn is_default_function_parameter_with_type_annotation(syntax: &SyntaxNode) -> bool { + syntax + .parent() + .and_then(JsInitializerClause::cast) + .and_then(|init| init.parent::()) + .is_some_and(|param| param.type_annotation().is_some()) +} + +/// Checks if a function is a class property with a type annotation. +/// +/// # Examples +/// +/// ```typescript +/// type MethodType = () => void; +/// class App { +/// private method: MethodType = () => { }; +/// } +/// ``` +fn is_class_property_with_type_annotation(syntax: &SyntaxNode) -> bool { + syntax + .parent() + .and_then(JsInitializerClause::cast) + .and_then(|init| init.parent::()) + .is_some_and(|prop| prop.property_annotation().is_some()) +} + +/// Checks if a function is a property or a nested property of a typed object. +/// +/// # Examples +/// +/// ```typescript +/// const x: Foo = { prop: () => {} } +/// const x = { prop: () => {} } as Foo +/// const x = { prop: () => {} } +/// const x: Foo = { bar: { prop: () => {} } } +/// ``` +fn is_property_of_object_with_type(syntax: &SyntaxNode) -> bool { + syntax + .parent() + .and_then(JsPropertyObjectMember::cast) + .and_then(|prop| prop.syntax().grand_parent()) + .and_then(JsObjectExpression::cast) + .is_some_and(|obj_expression| { + let obj_syntax = obj_expression.syntax(); + is_type_assertion(obj_syntax) + || is_variable_declarator_with_type_annotation(obj_syntax) + || is_property_of_object_with_type(obj_syntax) + }) +} + +/// Checks if a function has a type assertion. +/// +/// # Examples +/// +/// ```typescript +/// const asTyped = (() => '') as () => string; +/// const castTyped = <() => string>(() => ''); +/// ``` +fn is_type_assertion(syntax: &SyntaxNode) -> bool { + fn is_assertion_kind(kind: JsSyntaxKind) -> bool { + matches!( + kind, + JsSyntaxKind::TS_AS_EXPRESSION | JsSyntaxKind::TS_TYPE_ASSERTION_EXPRESSION + ) + } + + syntax.parent().map_or(false, |parent| { + if parent.kind() == JsSyntaxKind::JS_PARENTHESIZED_EXPRESSION { + parent + .parent() + .is_some_and(|grandparent| is_assertion_kind(grandparent.kind())) + } else { + is_assertion_kind(parent.kind()) + } + }) +} 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 16fe67e58b09..1cb6ba5f394e 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/invalid.ts @@ -89,4 +89,7 @@ function fn() { return function (): string { return str; }; -} \ No newline at end of file +} + +const x = { prop: () => {} } +const x = { bar: { prop: () => {} } } \ 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 9b84cc810709..f64962a5730d 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 @@ -96,6 +96,9 @@ function fn() { return str; }; } + +const x = { prop: () => {} } +const x = { bar: { prop: () => {} } } ``` # Diagnostics @@ -527,3 +530,37 @@ invalid.ts:85:2 lint/nursery/useExplicitFunctionReturnType ━━━━━━━ ``` + +``` +invalid.ts:94:19 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 92 │ } + 93 │ + > 94 │ const x = { prop: () => {} } + │ ^^^^^^^^^ + 95 │ const x = { bar: { prop: () => {} } } + + 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:95:26 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 94 │ const x = { prop: () => {} } + > 95 │ const x = { bar: { prop: () => {} } } + │ ^^^^^^^^^ + + i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking. + + i Add a return type annotation. + + +``` 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 0c381b08a967..41f4283c04c6 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitFunctionReturnType/valid.ts @@ -47,6 +47,8 @@ node.addEventListener('click', function () {}); const foo = arr.map(i => i * i); fn(() => {}); fn(function () {}); +new Promise(resolve => {}); +new Foo(1, () => {}); [function () {}, () => {}]; (function () { console.log("This is an IIFE"); @@ -62,4 +64,42 @@ const arrowFn = () => (): void => {}; const arrowFn = () => function(): void {} const arrowFn = () => { return (): void => { }; -} \ No newline at end of file +} + + +// type assertion +const asTyped = (() => '') as () => string; +const castTyped = <() => string>(() => ''); + +// variable declarator with a type annotation +type FuncType = () => string; +const arrowFn: FuncType = () => 'test'; +const funcExpr: FuncType = function () { + return 'test'; +}; + +// default parameter with a type annotation +type CallBack = () => void; +const f = (gotcha: CallBack = () => { }): void => { }; +function f(gotcha: CallBack = () => {}): void {} + +// class property with a type annotation +type MethodType = () => void; +class App { + private method: MethodType = () => { }; +} + +// function as a property or a nested property of a typed object +const x: Foo = { prop: () => {} } +const x = { prop: () => {} } as Foo +const x = { prop: () => {} } + +const x: Foo = { bar: { prop: () => {} } } + +class Accumulator { + private count: number = 0; + public accumulate(fn: () => number): void { + this.count += fn(); + } +} +new Accumulator().accumulate(() => 1); \ 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 80b8221a7297..45a4d83905d7 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 @@ -53,6 +53,8 @@ node.addEventListener('click', function () {}); const foo = arr.map(i => i * i); fn(() => {}); fn(function () {}); +new Promise(resolve => {}); +new Foo(1, () => {}); [function () {}, () => {}]; (function () { console.log("This is an IIFE"); @@ -69,4 +71,42 @@ const arrowFn = () => function(): void {} const arrowFn = () => { return (): void => { }; } + + +// type assertion +const asTyped = (() => '') as () => string; +const castTyped = <() => string>(() => ''); + +// variable declarator with a type annotation +type FuncType = () => string; +const arrowFn: FuncType = () => 'test'; +const funcExpr: FuncType = function () { + return 'test'; +}; + +// default parameter with a type annotation +type CallBack = () => void; +const f = (gotcha: CallBack = () => { }): void => { }; +function f(gotcha: CallBack = () => {}): void {} + +// class property with a type annotation +type MethodType = () => void; +class App { + private method: MethodType = () => { }; +} + +// function as a property or a nested property of a typed object +const x: Foo = { prop: () => {} } +const x = { prop: () => {} } as Foo +const x = { prop: () => {} } + +const x: Foo = { bar: { prop: () => {} } } + +class Accumulator { + private count: number = 0; + public accumulate(fn: () => number): void { + this.count += fn(); + } +} +new Accumulator().accumulate(() => 1); ```