diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index fcf1a4cabd0a..2471957bf53f 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs @@ -147,6 +147,14 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "@typescript-eslint/explicit-module-boundary-types" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group.use_explicit_type.get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "@typescript-eslint/naming-convention" => { if !options.include_inspired { results.has_inspired_rules = true; diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index 78d1a1f341d6..1b2d221c33b8 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3422,7 +3422,7 @@ pub struct Nursery { #[serde(skip_serializing_if = "Option::is_none")] pub use_deprecated_reason: Option>, - #[doc = "Require explicit return types on functions and class methods."] + #[doc = "Require explicit argument and return types on functions and class methods."] #[serde(skip_serializing_if = "Option::is_none")] pub use_explicit_type: Option>, #[doc = "Enforces the use of a recommended display strategy with Google Fonts."] diff --git a/crates/biome_js_analyze/src/lint/nursery/use_explicit_type.rs b/crates/biome_js_analyze/src/lint/nursery/use_explicit_type.rs index b7279dfe3a25..266da9eedeb7 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_explicit_type.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_explicit_type.rs @@ -1,7 +1,7 @@ use biome_analyze::{ context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, }; -use biome_console::markup; +use biome_console::{markup, MarkupBuf}; use biome_js_semantic::HasClosureAstNode; use biome_js_syntax::{ AnyJsBinding, AnyJsExpression, AnyJsFunctionBody, AnyJsStatement, AnyTsType, JsCallExpression, @@ -11,23 +11,23 @@ use biome_js_syntax::{ }; use biome_js_syntax::{ AnyJsFunction, JsGetterClassMember, JsGetterObjectMember, JsMethodClassMember, - JsMethodObjectMember, TsCallSignatureTypeMember, TsDeclareFunctionDeclaration, + JsMethodObjectMember, JsRestParameter, TsCallSignatureTypeMember, TsDeclareFunctionDeclaration, TsDeclareFunctionExportDefaultDeclaration, TsGetterSignatureClassMember, - TsMethodSignatureClassMember, TsMethodSignatureTypeMember, + TsMethodSignatureClassMember, TsMethodSignatureTypeMember, TsThisParameter, }; use biome_rowan::{declare_node_union, AstNode, SyntaxNode, SyntaxNodeOptionExt, TextRange}; declare_lint_rule! { - /// Require explicit return types on functions and class methods. + /// Require explicit argument and return types on functions and class methods. /// /// Functions in TypeScript often don't need to be given an explicit return type annotation. /// Leaving off the return type is less code to read or write and allows the compiler to infer it from the contents of the function. /// - /// However, explicit return types do make it visually more clear what type is returned by a function. + /// However, explicit argument and return types make it visually more clear what types a function accepts and returns. /// They can also speed up TypeScript type checking performance in large codebases with many large functions. - /// Explicit return types also reduce the chance of bugs by asserting the return type, and it avoids surprising "action at a distance," where changing the body of one function may cause failures inside another function. + /// Explicit types also reduce the chance of bugs by asserting both input and output types, and it avoids surprising "action at a distance," where changing the body of one function may cause failures inside another function. /// - /// This rule enforces that functions do have an explicit return type annotation. + /// This rule enforces that functions have explicit type annotations for both their arguments and return type. /// /// ## Examples /// @@ -156,6 +156,45 @@ declare_lint_rule! { /// } /// ``` /// + /// The following pattern is considered incorrect code for missing an argument type on an function: + /// + /// ```ts,expect_diagnostic + /// export function test(a: number, b): void { + /// return; + /// } + /// ``` + /// + /// ```ts,expect_diagnostic + /// export const test = (a): void => { + /// return; + /// } + /// ``` + /// + /// ```ts,expect_diagnostic + /// export default function test(a): void { + /// return; + /// } + /// ``` + /// + /// ```ts,expect_diagnostic + /// export default (a): void => { + /// return; + /// } + /// ``` + /// + /// ```ts,expect_diagnostic + /// class Test { + /// constructor(a) {} + /// } + /// ``` + /// + /// ```ts,expect_diagnostic + /// declare module "foo" { + /// export default function bar(a): string; + /// } + /// ``` + /// + /// /// ### Valid /// ```ts /// // No return value should be expected (void) @@ -258,12 +297,12 @@ declare_lint_rule! { name: "useExplicitType", language: "ts", recommended: false, - sources: &[RuleSource::EslintTypeScript("explicit-function-return-type")], + sources: &[RuleSource::EslintTypeScript("explicit-function-return-type"), RuleSource::EslintTypeScript("explicit-module-boundary-types")], } } declare_node_union! { - pub AnyCallableWithReturn = + pub FunctionSignaturePart = AnyJsFunction | JsMethodClassMember | JsMethodObjectMember @@ -275,11 +314,60 @@ declare_node_union! { | TsGetterSignatureClassMember | TsDeclareFunctionDeclaration | TsDeclareFunctionExportDefaultDeclaration + | JsFormalParameter + | JsRestParameter + | TsThisParameter +} + +pub enum UseExplicitTypeState { + MissingReturnType(TextRange), + MissingArgumentnType(TextRange, String), +} + +impl UseExplicitTypeState { + fn range(&self) -> &TextRange { + match &self { + UseExplicitTypeState::MissingReturnType(range) => range, + UseExplicitTypeState::MissingArgumentnType(range, _) => range, + } + } + fn title(&self) -> MarkupBuf { + match &self { + UseExplicitTypeState::MissingReturnType(_) => { + (markup! {"Missing return type on function."}).to_owned() + } + UseExplicitTypeState::MissingArgumentnType(_, name) => { + (markup! {"Argument '"{name}"' should be typed."}).to_owned() + } + } + } + + fn note_reason(&self) -> MarkupBuf { + match &self { + UseExplicitTypeState::MissingReturnType(_) => { + (markup! {"Declaring the return type makes the code self-documenting and can speed up TypeScript type checking."}).to_owned() + } + UseExplicitTypeState::MissingArgumentnType(_, _) => { + (markup! {"Declaring the argument types makes the code self-documenting and can speed up TypeScript type checking."}).to_owned() + } + } + } + + fn note_action(&self) -> MarkupBuf { + match &self { + UseExplicitTypeState::MissingReturnType(_) => { + (markup! {"Add a return type annotation."}).to_owned() + } + UseExplicitTypeState::MissingArgumentnType(_, _) => { + (markup! {"Add type annotations to the function arguments."}).to_owned() + } + } + } } impl Rule for UseExplicitType { - type Query = Ast; - type State = TextRange; + type Query = Ast; + type State = UseExplicitTypeState; type Signals = Option; type Options = (); @@ -291,7 +379,7 @@ impl Rule for UseExplicitType { let node = ctx.query(); match node { - AnyCallableWithReturn::AnyJsFunction(func) => { + FunctionSignaturePart::AnyJsFunction(func) => { if func.return_type_annotation().is_some() { return None; } @@ -318,97 +406,122 @@ impl Rule for UseExplicitType { let func_range = func.syntax().text_range(); if let Ok(Some(AnyJsBinding::JsIdentifierBinding(id))) = func.id() { - return Some(TextRange::new( + return Some(UseExplicitTypeState::MissingReturnType(TextRange::new( func_range.start(), id.syntax().text_range().end(), - )); + ))); } - Some(func_range) + Some(UseExplicitTypeState::MissingReturnType(func_range)) } - AnyCallableWithReturn::JsMethodClassMember(method) => { + FunctionSignaturePart::JsMethodClassMember(method) => { if method.return_type_annotation().is_some() { return None; } - Some(method.node_text_range()) + Some(UseExplicitTypeState::MissingReturnType( + method.node_text_range(), + )) } - AnyCallableWithReturn::JsGetterClassMember(getter) => { + FunctionSignaturePart::JsGetterClassMember(getter) => { if getter.return_type().is_some() { return None; } - Some(getter.node_text_range()) + Some(UseExplicitTypeState::MissingReturnType( + getter.node_text_range(), + )) } - AnyCallableWithReturn::JsMethodObjectMember(method) => { + FunctionSignaturePart::JsMethodObjectMember(method) => { if method.return_type_annotation().is_some() { return None; } - Some(method.node_text_range()) + Some(UseExplicitTypeState::MissingReturnType( + method.node_text_range(), + )) } - AnyCallableWithReturn::JsGetterObjectMember(getter) => { + FunctionSignaturePart::JsGetterObjectMember(getter) => { if getter.return_type().is_some() { return None; } - Some(getter.node_text_range()) + Some(UseExplicitTypeState::MissingReturnType( + getter.node_text_range(), + )) } - AnyCallableWithReturn::TsMethodSignatureTypeMember(member) => { + FunctionSignaturePart::TsMethodSignatureTypeMember(member) => { if member.return_type_annotation().is_some() { return None; } - Some(member.range()) + Some(UseExplicitTypeState::MissingReturnType(member.range())) } - AnyCallableWithReturn::TsCallSignatureTypeMember(member) => { + FunctionSignaturePart::TsCallSignatureTypeMember(member) => { if member.return_type_annotation().is_some() { return None; } - Some(member.range()) + Some(UseExplicitTypeState::MissingReturnType(member.range())) } - AnyCallableWithReturn::TsMethodSignatureClassMember(member) => { + FunctionSignaturePart::TsMethodSignatureClassMember(member) => { if member.return_type_annotation().is_some() { return None; } - Some(member.range()) + Some(UseExplicitTypeState::MissingReturnType(member.range())) } - AnyCallableWithReturn::TsGetterSignatureClassMember(member) => { + FunctionSignaturePart::TsGetterSignatureClassMember(member) => { if member.return_type().is_some() { return None; } - Some(member.range()) + Some(UseExplicitTypeState::MissingReturnType(member.range())) } - AnyCallableWithReturn::TsDeclareFunctionDeclaration(decl) => { + FunctionSignaturePart::TsDeclareFunctionDeclaration(decl) => { if decl.return_type_annotation().is_some() { return None; } - Some(decl.range()) + Some(UseExplicitTypeState::MissingReturnType(decl.range())) } - AnyCallableWithReturn::TsDeclareFunctionExportDefaultDeclaration(decl) => { + FunctionSignaturePart::TsDeclareFunctionExportDefaultDeclaration(decl) => { if decl.return_type_annotation().is_some() { return None; } - Some(decl.range()) + Some(UseExplicitTypeState::MissingReturnType(decl.range())) + } + FunctionSignaturePart::JsFormalParameter(param) => { + if param.type_annotation().is_some() { + return None; + } + Some(UseExplicitTypeState::MissingArgumentnType( + param.range(), + param.text(), + )) + } + FunctionSignaturePart::JsRestParameter(param) => { + if param.type_annotation().is_some() { + return None; + } + Some(UseExplicitTypeState::MissingArgumentnType( + param.range(), + param.text(), + )) + } + FunctionSignaturePart::TsThisParameter(param) => { + if param.type_annotation().is_some() { + return None; + } + Some(UseExplicitTypeState::MissingArgumentnType( + param.range(), + param.text(), + )) } } } fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { Some( - RuleDiagnostic::new( - rule_category!(), - state, - markup! { - "Missing return type on function." - }, - ) - .note(markup! { - "Declaring the return type makes the code self-documenting and can speed up TypeScript type checking." - }) - .note(markup! { - "Add a return type annotation." - }), + RuleDiagnostic::new(rule_category!(), state.range(), state.title()) + .note(state.note_reason()) + .note(state.note_action()), ) } } diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/invalid.ts index 0e1dc3e0620a..f968a641d7a5 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/invalid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/invalid.ts @@ -121,3 +121,47 @@ declare namespace myLib { declare module "foo" { export default function bar(); } + +export default (a): void => { + return; +}; +export function test(a: number, b) { + return; +} + +class Human { + private name: string; + + public toString(this): string { + return `Name ${this.name}`; + } +} + +interface Array { + pop(): Type | undefined; + push(...items): number; +} + +type MyObject = { + (input); + propertyName: string; +}; + +export function test({ a, b }): string { + return; +} + +declare module "foo" { + export default function bar(a): string; +} + +declare namespace myLib { + function makeGreeting(s): string; +} + +export class Test { + constructor(a) {} + method(a): void { + return; + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/invalid.ts.snap index df78ed6e20f0..b7d300566a28 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/invalid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/invalid.ts.snap @@ -129,6 +129,50 @@ declare module "foo" { export default function bar(); } +export default (a): void => { + return; +}; +export function test(a: number, b) { + return; +} + +class Human { + private name: string; + + public toString(this): string { + return `Name ${this.name}`; + } +} + +interface Array { + pop(): Type | undefined; + push(...items): number; +} + +type MyObject = { + (input); + propertyName: string; +}; + +export function test({ a, b }): string { + return; +} + +declare module "foo" { + export default function bar(a): string; +} + +declare namespace myLib { + function makeGreeting(s): string; +} + +export class Test { + constructor(a) {} + method(a): void { + return; + } +} + ``` # Diagnostics @@ -707,3 +751,226 @@ invalid.ts:122:17 lint/nursery/useExplicitType ━━━━━━━━━━━ ``` + +``` +invalid.ts:125:17 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Argument 'a' should be typed. + + 123 │ } + 124 │ + > 125 │ export default (a): void => { + │ ^ + 126 │ return; + 127 │ }; + + i Declaring the argument types makes the code self-documenting and can speed up TypeScript type checking. + + i Add type annotations to the function arguments. + + +``` + +``` +invalid.ts:128:8 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 126 │ return; + 127 │ }; + > 128 │ export function test(a: number, b) { + │ ^^^^^^^^^^^^^ + 129 │ return; + 130 │ } + + 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:128:33 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Argument 'b' should be typed. + + 126 │ return; + 127 │ }; + > 128 │ export function test(a: number, b) { + │ ^ + 129 │ return; + 130 │ } + + i Declaring the argument types makes the code self-documenting and can speed up TypeScript type checking. + + i Add type annotations to the function arguments. + + +``` + +``` +invalid.ts:135:18 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Argument 'this' should be typed. + + 133 │ private name: string; + 134 │ + > 135 │ public toString(this): string { + │ ^^^^ + 136 │ return `Name ${this.name}`; + 137 │ } + + i Declaring the argument types makes the code self-documenting and can speed up TypeScript type checking. + + i Add type annotations to the function arguments. + + +``` + +``` +invalid.ts:142:7 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Argument '...items' should be typed. + + 140 │ interface Array { + 141 │ pop(): Type | undefined; + > 142 │ push(...items): number; + │ ^^^^^^^^ + 143 │ } + 144 │ + + i Declaring the argument types makes the code self-documenting and can speed up TypeScript type checking. + + i Add type annotations to the function arguments. + + +``` + +``` +invalid.ts:146:2 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Missing return type on function. + + 145 │ type MyObject = { + > 146 │ (input); + │ ^^^^^^^^ + 147 │ propertyName: string; + 148 │ }; + + 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:146:3 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Argument 'input' should be typed. + + 145 │ type MyObject = { + > 146 │ (input); + │ ^^^^^ + 147 │ propertyName: string; + 148 │ }; + + i Declaring the argument types makes the code self-documenting and can speed up TypeScript type checking. + + i Add type annotations to the function arguments. + + +``` + +``` +invalid.ts:150:22 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Argument '{ a, b }' should be typed. + + 148 │ }; + 149 │ + > 150 │ export function test({ a, b }): string { + │ ^^^^^^^^ + 151 │ return; + 152 │ } + + i Declaring the argument types makes the code self-documenting and can speed up TypeScript type checking. + + i Add type annotations to the function arguments. + + +``` + +``` +invalid.ts:155:30 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Argument 'a' should be typed. + + 154 │ declare module "foo" { + > 155 │ export default function bar(a): string; + │ ^ + 156 │ } + 157 │ + + i Declaring the argument types makes the code self-documenting and can speed up TypeScript type checking. + + i Add type annotations to the function arguments. + + +``` + +``` +invalid.ts:159:24 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Argument 's' should be typed. + + 158 │ declare namespace myLib { + > 159 │ function makeGreeting(s): string; + │ ^ + 160 │ } + 161 │ + + i Declaring the argument types makes the code self-documenting and can speed up TypeScript type checking. + + i Add type annotations to the function arguments. + + +``` + +``` +invalid.ts:163:14 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Argument 'a' should be typed. + + 162 │ export class Test { + > 163 │ constructor(a) {} + │ ^ + 164 │ method(a): void { + 165 │ return; + + i Declaring the argument types makes the code self-documenting and can speed up TypeScript type checking. + + i Add type annotations to the function arguments. + + +``` + +``` +invalid.ts:164:9 lint/nursery/useExplicitType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Argument 'a' should be typed. + + 162 │ export class Test { + 163 │ constructor(a) {} + > 164 │ method(a): void { + │ ^ + 165 │ return; + 166 │ } + + i Declaring the argument types makes the code self-documenting and can speed up TypeScript type checking. + + i Add type annotations to the function arguments. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/valid.ts b/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/valid.ts index 21cb693c5484..9860efd749a8 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/valid.ts +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/valid.ts @@ -43,10 +43,10 @@ const func = () => x as const; // check allow expressions node.addEventListener("click", () => {}); node.addEventListener("click", function () {}); -const foo = arr.map((i) => i * i); +const foo = arr.map((i: number) => i * i); fn(() => {}); fn(function () {}); -new Promise((resolve) => {}); +new Promise((resolve: () => void) => {}); new Foo(1, () => {}); [function () {}, () => {}]; (function () { @@ -129,3 +129,18 @@ declare namespace myLib { declare module "foo" { export default function bar(): string; } + +export default function test(obj: { a: string }): void { + return; +} +export function add(a: number, b: number): number { + return a + b; +} + +class Human { + private name: string; + + public toString(this: Human): string { + return `Name ${this.name}`; + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/valid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/valid.ts.snap index c23c5c68072e..624bf0e50e94 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/valid.ts.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/useExplicitType/valid.ts.snap @@ -50,10 +50,10 @@ const func = () => x as const; // check allow expressions node.addEventListener("click", () => {}); node.addEventListener("click", function () {}); -const foo = arr.map((i) => i * i); +const foo = arr.map((i: number) => i * i); fn(() => {}); fn(function () {}); -new Promise((resolve) => {}); +new Promise((resolve: () => void) => {}); new Foo(1, () => {}); [function () {}, () => {}]; (function () { @@ -137,4 +137,19 @@ declare module "foo" { export default function bar(): string; } +export default function test(obj: { a: string }): void { + return; +} +export function add(a: number, b: number): number { + return a + b; +} + +class Human { + private name: string; + + public toString(this: Human): string { + return `Name ${this.name}`; + } +} + ``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index d8b88ee311d4..b8cc33401d07 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1395,7 +1395,7 @@ export interface Nursery { */ useDeprecatedReason?: RuleConfiguration_for_Null; /** - * Require explicit return types on functions and class methods. + * Require explicit argument and return types on functions and class methods. */ useExplicitType?: RuleConfiguration_for_Null; /** diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index aa8152c643a1..8287e45bd1a7 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2469,7 +2469,7 @@ ] }, "useExplicitType": { - "description": "Require explicit return types on functions and class methods.", + "description": "Require explicit argument and return types on functions and class methods.", "anyOf": [ { "$ref": "#/definitions/RuleConfiguration" }, { "type": "null" }