Skip to content

Commit

Permalink
feat(lint/noEvolvingAny): add rule (#2112)
Browse files Browse the repository at this point in the history
  • Loading branch information
fujiyamaorange authored Mar 23, 2024
1 parent 091854f commit a2027da
Show file tree
Hide file tree
Showing 16 changed files with 537 additions and 39 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ our [guidelines for writing a good changelog entry](https:/biomejs/b

### Linter

#### New features

- Add rule [noEvolvingAny](https://biomejs.dev/linter/rules/no-evolving-any) to disallow variables from evolving into `any` type through reassignments.

Contributed by @fujiyamaorange

#### Bug fixes

- Rule `noUndeclaredDependencies` now also validates `peerDependencies` and `optionalDependencies` ([#2122](https:/biomejs/biome/issues/2122)). Contributed by @Sec-ant
Expand Down
1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ define_categories! {
"lint/nursery/noDuplicateElseIf": "https://biomejs.dev/linter/rules/no-duplicate-else-if",
"lint/nursery/noDuplicateJsonKeys": "https://biomejs.dev/linter/rules/no-duplicate-json-keys",
"lint/nursery/noDuplicateTestHooks": "https://biomejs.dev/linter/rules/no-duplicate-test-hooks",
"lint/nursery/noEvolvingAny": "https://biomejs.dev/linter/rules/no-evolving-any",
"lint/nursery/noExcessiveNestedTestSuites": "https://biomejs.dev/linter/rules/no-excessive-nested-test-suites",
"lint/nursery/noExportsInTest": "https://biomejs.dev/linter/rules/no-exports-in-test",
"lint/nursery/noFocusedTests": "https://biomejs.dev/linter/rules/no-focused-tests",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod no_console;
pub mod no_done_callback;
pub mod no_duplicate_else_if;
pub mod no_duplicate_test_hooks;
pub mod no_evolving_any;
pub mod no_excessive_nested_test_suites;
pub mod no_exports_in_test;
pub mod no_focused_tests;
Expand All @@ -32,6 +33,7 @@ declare_group! {
self :: no_done_callback :: NoDoneCallback ,
self :: no_duplicate_else_if :: NoDuplicateElseIf ,
self :: no_duplicate_test_hooks :: NoDuplicateTestHooks ,
self :: no_evolving_any :: NoEvolvingAny ,
self :: no_excessive_nested_test_suites :: NoExcessiveNestedTestSuites ,
self :: no_exports_in_test :: NoExportsInTest ,
self :: no_focused_tests :: NoFocusedTests ,
Expand Down
123 changes: 123 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/no_evolving_any.rs
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."
}),
)
}
}
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ pub type NoEmptyInterface =
pub type NoEmptyPattern =
<lint::correctness::no_empty_pattern::NoEmptyPattern as biome_analyze::Rule>::Options;
pub type NoEmptyTypeParameters = < lint :: complexity :: no_empty_type_parameters :: NoEmptyTypeParameters as biome_analyze :: Rule > :: Options ;
pub type NoEvolvingAny =
<lint::nursery::no_evolving_any::NoEvolvingAny as biome_analyze::Rule>::Options;
pub type NoExcessiveCognitiveComplexity = < lint :: complexity :: no_excessive_cognitive_complexity :: NoExcessiveCognitiveComplexity as biome_analyze :: Rule > :: Options ;
pub type NoExcessiveNestedTestSuites = < lint :: nursery :: no_excessive_nested_test_suites :: NoExcessiveNestedTestSuites as biome_analyze :: Rule > :: Options ;
pub type NoExplicitAny =
Expand Down
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;
}
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 crates/biome_js_analyze/tests/specs/nursery/noEvolvingAny/valid.ts
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();
Loading

0 comments on commit a2027da

Please sign in to comment.