Skip to content

Commit

Permalink
feat(linter): support allow expressions (#4036)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaykdm authored Sep 23, 2024
1 parent f0ff7eb commit 38b8b43
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ use biome_analyze::{
};
use biome_console::markup;
use biome_js_semantic::HasClosureAstNode;
use biome_js_syntax::{AnyJsBinding, AnyJsExpression, AnyJsFunctionBody, AnyTsType};
use biome_js_syntax::{AnyJsBinding, AnyJsExpression, AnyJsFunctionBody, AnyTsType, JsSyntaxKind};
use biome_js_syntax::{
AnyJsFunction, JsGetterClassMember, JsGetterObjectMember, JsMethodClassMember,
JsMethodObjectMember,
};
use biome_rowan::{declare_node_union, AstNode, TextRange};
use biome_rowan::{declare_node_union, AstNode, SyntaxNodeOptionExt, TextRange};

declare_lint_rule! {
/// Require explicit return types on functions and class methods.
Expand Down Expand Up @@ -99,6 +99,15 @@ declare_lint_rule! {
/// const func = (value: number) => ({ foo: 'bar', value }) as const;
/// ```
///
/// ```ts
/// // Callbacks without return types
/// setTimeout(function() { console.log("Hello!"); }, 1000);
/// ```
/// ```ts
/// // IIFE
/// (() => {})();
/// ```
///
pub UseExplicitFunctionReturnType {
version: "next",
name: "useExplicitFunctionReturnType",
Expand Down Expand Up @@ -130,6 +139,10 @@ impl Rule for UseExplicitFunctionReturnType {
return None;
}

if is_function_used_in_argument_or_expression_list(func) {
return None;
}

let func_range = func.syntax().text_range();
if let Ok(Some(AnyJsBinding::JsIdentifierBinding(id))) = func.id() {
return Some(TextRange::new(
Expand Down Expand Up @@ -190,11 +203,14 @@ impl Rule for UseExplicitFunctionReturnType {
}
}

/**
* Checks if an arrow function immediately returns a `as const` value.
* const func = (value: number) => ({ foo: 'bar', value }) as const;
* const func = () => x as const;
*/
/// Checks if an arrow function immediately returns an `as const` value.
///
/// # Examples
///
/// ```typescript
/// const func = (value: number) => ({ foo: 'bar', value }) as const;
/// const func = () => x as const;
/// ```
fn is_direct_const_assertion_in_arrow_functions(func: &AnyJsFunction) -> bool {
let AnyJsFunction::JsArrowFunctionExpression(arrow_func) = func else {
return false;
Expand All @@ -214,3 +230,32 @@ fn is_direct_const_assertion_in_arrow_functions(func: &AnyJsFunction) -> bool {

ts_ref.text() == "const"
}

/// Checks if a function is allowed within specific expression contexts.
/// These include function calls, array elements, and parenthesized expressions.
///
/// # Examples
///
/// JS_CALL_ARGUMENT_LIST:
/// - `window.addEventListener('click', () => {});`
/// - `const foo = arr.map(i => i * i);`
/// - `setTimeout(function() { console.log("Hello!"); }, 1000);`
///
/// JS_ARRAY_ELEMENT_LIST:
/// - `[function () {}, () => {}];`
///
/// JS_PARENTHESIZED_EXPRESSION:
/// - `(function () {});`
/// - `(() => {})();`
fn is_function_used_in_argument_or_expression_list(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
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ const obj = {
};

const func = (value: number) => ({ type: 'X', value }) as any;
const func = (value: number) => ({ type: 'X', value }) as Action;
const func = (value: number) => ({ type: 'X', value }) as Action;

export default () => {};
export default function () {}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ const obj = {

const func = (value: number) => ({ type: 'X', value }) as any;
const func = (value: number) => ({ type: 'X', value }) as Action;

export default () => {};
export default function () {}
```

# Diagnostics
Expand Down Expand Up @@ -267,6 +270,7 @@ invalid.ts:42:14 lint/nursery/useExplicitFunctionReturnType ━━━━━━
> 42 │ const func = (value: number) => ({ type: 'X', value }) as any;
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
43 │ const func = (value: number) => ({ type: 'X', value }) as Action;
44 │
i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking.
Expand All @@ -283,6 +287,42 @@ invalid.ts:43:14 lint/nursery/useExplicitFunctionReturnType ━━━━━━
42 │ const func = (value: number) => ({ type: 'X', value }) as any;
> 43 │ const func = (value: number) => ({ type: 'X', value }) as Action;
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44 │
45 │ export default () => {};
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:45:16 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Missing return type on function.
43 │ const func = (value: number) => ({ type: 'X', value }) as Action;
44 │
> 45 │ export default () => {};
│ ^^^^^^^^
46 │ export default function () {}
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:46:16 lint/nursery/useExplicitFunctionReturnType ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Missing return type on function.
45 │ export default () => {};
> 46 │ export default function () {}
│ ^^^^^^^^^^^^^^
i Declaring the return type makes the code self-documenting and can speed up TypeScript type checking.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,25 @@ const obj = {
},
};

export default (): void => {};
export default function (): void {}

// check direct const assertions
const func = (value: number) => ({ foo: 'bar', value }) as const;
const func = () => x as const;
const func = () => x as const;


// check allow expressions
node.addEventListener('click', () => {});
node.addEventListener('click', function () {});
const foo = arr.map(i => i * i);
fn(() => {});
fn(function () {});
[function () {}, () => {}];
(function () {
console.log("This is an IIFE");
})();
(() => {
console.log("This is an IIFE");
})();
setTimeout(function() { console.log("Hello!"); }, 1000);
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,26 @@ const obj = {
},
};

export default (): void => {};
export default function (): void {}

// check direct const assertions
const func = (value: number) => ({ foo: 'bar', value }) as const;
const func = () => x as const;


// check allow expressions
node.addEventListener('click', () => {});
node.addEventListener('click', function () {});
const foo = arr.map(i => i * i);
fn(() => {});
fn(function () {});
[function () {}, () => {}];
(function () {
console.log("This is an IIFE");
})();
(() => {
console.log("This is an IIFE");
})();
setTimeout(function() { console.log("Hello!"); }, 1000);
```

0 comments on commit 38b8b43

Please sign in to comment.