-
-
Notifications
You must be signed in to change notification settings - Fork 511
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(lint/noEvolvingAny): add rule (#2112)
- Loading branch information
1 parent
091854f
commit a2027da
Showing
16 changed files
with
537 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
crates/biome_js_analyze/src/lint/nursery/no_evolving_any.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
use biome_analyze::{context::RuleContext, declare_rule, Ast, Rule, RuleDiagnostic}; | ||
use biome_console::markup; | ||
use biome_js_syntax::{AnyJsExpression, JsFileSource, JsVariableDeclaration, JsVariableDeclarator}; | ||
|
||
declare_rule! { | ||
/// Disallow variables from evolving into `any` type through reassignments. | ||
/// | ||
/// In TypeScript, variables without explicit type annotations can evolve their types based on subsequent assignments. | ||
/// This behaviour can accidentally lead to variables with an `any` type, weakening type safety. | ||
/// Just like the `any` type, evolved `any` types disable many type-checking rules and should be avoided to maintain strong type safety. | ||
/// This rule prevents such cases by ensuring variables do not evolve into `any` type, encouraging explicit type annotations and controlled type evolutions. | ||
/// | ||
/// ## Examples | ||
/// | ||
/// ### Invalid | ||
/// | ||
/// ```ts,expect_diagnostic | ||
/// let a; | ||
/// ```` | ||
/// | ||
/// ```ts,expect_diagnostic | ||
/// const b = []; | ||
/// ```` | ||
/// | ||
/// ```ts,expect_diagnostic | ||
/// let c = null; | ||
/// ```` | ||
/// | ||
/// | ||
/// ### Valid | ||
/// | ||
/// ```ts | ||
/// let a: number; | ||
/// let b = 1; | ||
/// var c : string; | ||
/// var d = "abn"; | ||
/// const e: never[] = []; | ||
/// const f = [null]; | ||
/// const g = ['1']; | ||
/// const h = [1]; | ||
/// let workspace: Workspace | null = null; | ||
/// ``` | ||
/// | ||
pub NoEvolvingAny { | ||
version: "next", | ||
name: "noEvolvingAny", | ||
recommended: true, | ||
} | ||
} | ||
|
||
impl Rule for NoEvolvingAny { | ||
type Query = Ast<JsVariableDeclaration>; | ||
type State = JsVariableDeclarator; | ||
type Signals = Option<Self::State>; | ||
type Options = (); | ||
|
||
fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
let source_type = ctx.source_type::<JsFileSource>().language(); | ||
let is_ts_source = source_type.is_typescript(); | ||
let node = ctx.query(); | ||
let is_declaration = source_type.is_definition_file(); | ||
|
||
if is_declaration || !is_ts_source { | ||
return None; | ||
} | ||
|
||
for declarator in node.declarators() { | ||
let variable = declarator.ok()?; | ||
|
||
let is_initialized = variable.initializer().is_some(); | ||
let is_type_annotated = variable.variable_annotation().is_some(); | ||
|
||
if !is_initialized && !is_type_annotated { | ||
return Some(variable); | ||
} | ||
|
||
if is_initialized { | ||
let initializer = variable.initializer()?; | ||
let expression = initializer.expression().ok()?; | ||
match expression { | ||
AnyJsExpression::AnyJsLiteralExpression(literal_expr) => { | ||
if literal_expr.as_js_null_literal_expression().is_some() | ||
&& !is_type_annotated | ||
{ | ||
return Some(variable); | ||
} | ||
} | ||
AnyJsExpression::JsArrayExpression(array_expr) => { | ||
if array_expr.elements().into_iter().next().is_none() && !is_type_annotated | ||
{ | ||
return Some(variable); | ||
} | ||
} | ||
_ => continue, | ||
}; | ||
} | ||
} | ||
|
||
None | ||
} | ||
|
||
fn diagnostic(_: &RuleContext<Self>, node: &Self::State) -> Option<RuleDiagnostic> { | ||
let variable = node | ||
.id() | ||
.ok()? | ||
.as_any_js_binding()? | ||
.as_js_identifier_binding()? | ||
.name_token() | ||
.ok()?; | ||
Some( | ||
RuleDiagnostic::new( | ||
rule_category!(), | ||
variable.text_trimmed_range(), | ||
markup! { | ||
"This variable's type is not allowed to evolve implicitly, leading to potential "<Emphasis>"any"</Emphasis>" types." | ||
}, | ||
) | ||
.note(markup! { | ||
"The variable's type may evolve, leading to "<Emphasis>"any"</Emphasis>". Use explicit type or initialization. Specifying an explicit type or initial value to avoid implicit type evolution." | ||
}), | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
crates/biome_js_analyze/tests/specs/nursery/noEvolvingAny/invalid.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
let a; | ||
const b = []; | ||
let c = null; | ||
|
||
let someVar1; | ||
someVar1 = '123'; | ||
someVar1 = 123; | ||
|
||
var someVar1; | ||
someVar1 = '123'; | ||
someVar1 = 123; | ||
|
||
let x = 0, y, z = 0; | ||
var x = 0, y, z = 0; | ||
for(let a = 0, b; a < 5; a++) {} | ||
|
||
function ex() { | ||
let b; | ||
} |
176 changes: 176 additions & 0 deletions
176
crates/biome_js_analyze/tests/specs/nursery/noEvolvingAny/invalid.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
--- | ||
source: crates/biome_js_analyze/tests/spec_tests.rs | ||
expression: invalid.ts | ||
--- | ||
# Input | ||
```ts | ||
let a; | ||
const b = []; | ||
let c = null; | ||
|
||
let someVar1; | ||
someVar1 = '123'; | ||
someVar1 = 123; | ||
|
||
var someVar1; | ||
someVar1 = '123'; | ||
someVar1 = 123; | ||
|
||
let x = 0, y, z = 0; | ||
var x = 0, y, z = 0; | ||
for(let a = 0, b; a < 5; a++) {} | ||
|
||
function ex() { | ||
let b; | ||
} | ||
|
||
``` | ||
|
||
# Diagnostics | ||
``` | ||
invalid.ts:1:5 lint/nursery/noEvolvingAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
! This variable's type is not allowed to evolve implicitly, leading to potential any types. | ||
> 1 │ let a; | ||
│ ^ | ||
2 │ const b = []; | ||
3 │ let c = null; | ||
i The variable's type may evolve, leading to any. Use explicit type or initialization. Specifying an explicit type or initial value to avoid implicit type evolution. | ||
``` | ||
|
||
``` | ||
invalid.ts:2:7 lint/nursery/noEvolvingAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
! This variable's type is not allowed to evolve implicitly, leading to potential any types. | ||
1 │ let a; | ||
> 2 │ const b = []; | ||
│ ^ | ||
3 │ let c = null; | ||
4 │ | ||
i The variable's type may evolve, leading to any. Use explicit type or initialization. Specifying an explicit type or initial value to avoid implicit type evolution. | ||
``` | ||
|
||
``` | ||
invalid.ts:3:5 lint/nursery/noEvolvingAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
! This variable's type is not allowed to evolve implicitly, leading to potential any types. | ||
1 │ let a; | ||
2 │ const b = []; | ||
> 3 │ let c = null; | ||
│ ^ | ||
4 │ | ||
5 │ let someVar1; | ||
i The variable's type may evolve, leading to any. Use explicit type or initialization. Specifying an explicit type or initial value to avoid implicit type evolution. | ||
``` | ||
|
||
``` | ||
invalid.ts:5:5 lint/nursery/noEvolvingAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
! This variable's type is not allowed to evolve implicitly, leading to potential any types. | ||
3 │ let c = null; | ||
4 │ | ||
> 5 │ let someVar1; | ||
│ ^^^^^^^^ | ||
6 │ someVar1 = '123'; | ||
7 │ someVar1 = 123; | ||
i The variable's type may evolve, leading to any. Use explicit type or initialization. Specifying an explicit type or initial value to avoid implicit type evolution. | ||
``` | ||
|
||
``` | ||
invalid.ts:9:5 lint/nursery/noEvolvingAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
! This variable's type is not allowed to evolve implicitly, leading to potential any types. | ||
7 │ someVar1 = 123; | ||
8 │ | ||
> 9 │ var someVar1; | ||
│ ^^^^^^^^ | ||
10 │ someVar1 = '123'; | ||
11 │ someVar1 = 123; | ||
i The variable's type may evolve, leading to any. Use explicit type or initialization. Specifying an explicit type or initial value to avoid implicit type evolution. | ||
``` | ||
|
||
``` | ||
invalid.ts:13:12 lint/nursery/noEvolvingAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
! This variable's type is not allowed to evolve implicitly, leading to potential any types. | ||
11 │ someVar1 = 123; | ||
12 │ | ||
> 13 │ let x = 0, y, z = 0; | ||
│ ^ | ||
14 │ var x = 0, y, z = 0; | ||
15 │ for(let a = 0, b; a < 5; a++) {} | ||
i The variable's type may evolve, leading to any. Use explicit type or initialization. Specifying an explicit type or initial value to avoid implicit type evolution. | ||
``` | ||
|
||
``` | ||
invalid.ts:14:12 lint/nursery/noEvolvingAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
! This variable's type is not allowed to evolve implicitly, leading to potential any types. | ||
13 │ let x = 0, y, z = 0; | ||
> 14 │ var x = 0, y, z = 0; | ||
│ ^ | ||
15 │ for(let a = 0, b; a < 5; a++) {} | ||
16 │ | ||
i The variable's type may evolve, leading to any. Use explicit type or initialization. Specifying an explicit type or initial value to avoid implicit type evolution. | ||
``` | ||
|
||
``` | ||
invalid.ts:15:16 lint/nursery/noEvolvingAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
! This variable's type is not allowed to evolve implicitly, leading to potential any types. | ||
13 │ let x = 0, y, z = 0; | ||
14 │ var x = 0, y, z = 0; | ||
> 15 │ for(let a = 0, b; a < 5; a++) {} | ||
│ ^ | ||
16 │ | ||
17 │ function ex() { | ||
i The variable's type may evolve, leading to any. Use explicit type or initialization. Specifying an explicit type or initial value to avoid implicit type evolution. | ||
``` | ||
|
||
``` | ||
invalid.ts:18:9 lint/nursery/noEvolvingAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
! This variable's type is not allowed to evolve implicitly, leading to potential any types. | ||
17 │ function ex() { | ||
> 18 │ let b; | ||
│ ^ | ||
19 │ } | ||
20 │ | ||
i The variable's type may evolve, leading to any. Use explicit type or initialization. Specifying an explicit type or initial value to avoid implicit type evolution. | ||
``` |
15 changes: 15 additions & 0 deletions
15
crates/biome_js_analyze/tests/specs/nursery/noEvolvingAny/valid.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/* should not generate diagnostics */ | ||
|
||
let a: number; | ||
let b = 1 | ||
var c : string; | ||
var d = "abn" | ||
const e: never[] = []; | ||
const f = [null]; | ||
const g = ['1']; | ||
const h = [1]; | ||
let workspace: Workspace | null = null; | ||
|
||
const x = 0; | ||
for(let y of xs) {} | ||
using z = f(); |
Oops, something went wrong.