Skip to content

Commit 069ba72

Browse files
ematipicoautofix-ci[bot]
authored andcommitted
fix(inference): improved inference for assignment types (biomejs#8119)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent c7c92b9 commit 069ba72

File tree

11 files changed

+384
-10
lines changed

11 files changed

+384
-10
lines changed

.changeset/lovely-sloths-chew.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Improved the detection of the rule `noUnnecessaryConditions`. Now the rule isn't triggered for variables that are mutated inside a module.
6+
7+
This logic deviates from the original rule, hence `noUnnecessaryConditions` is now marked as "inspired".
8+
9+
In the following example, `hey` starts as `false`, but then it's assigned to a string. The rule isn't triggered inside the `if` check.
10+
11+
```js
12+
let hey = false;
13+
14+
function test() {
15+
hey = "string";
16+
}
17+
18+
if (hey) {}
19+
20+
```

.changeset/upset-cameras-walk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Improved the type inference engine, by resolving types for variables that are assigned to multiple values.

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_js_analyze/src/lint/nursery/no_unnecessary_conditions.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,22 @@ declare_lint_rule! {
4444
/// }
4545
/// ```
4646
///
47+
/// Contrary to the source rule, this rule doesn't trigger bindings that are assigned to multiple
48+
/// values. In the following example, the variable `greeting` is assigned to multiple values; hence
49+
/// it can't be inferred to a truthy or falsy value.
50+
///
51+
/// ```ts
52+
/// let greeting = false;
53+
///
54+
/// function changeGreeting() {
55+
/// greeting = "Hello World!"
56+
/// }
57+
///
58+
/// if (greeting) {} // rule not triggered here
59+
///
60+
/// ```
61+
///
62+
///
4763
/// ### Valid
4864
///
4965
/// ```ts
@@ -71,7 +87,7 @@ declare_lint_rule! {
7187
version: "2.1.4",
7288
name: "noUnnecessaryConditions",
7389
language: "js",
74-
sources: &[RuleSource::EslintTypeScript("no-unnecessary-condition").same()],
90+
sources: &[RuleSource::EslintTypeScript("no-unnecessary-condition").inspired()],
7591
recommended: false,
7692
severity: Severity::Warning,
7793
domains: &[RuleDomain::Project],
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// should not generate diagnostics
2+
3+
let hey = false;
4+
5+
function test() {
6+
hey = "string"
7+
}
8+
9+
if (hey) {}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: validAssignment.ts
4+
---
5+
# Input
6+
```ts
7+
// should not generate diagnostics
8+
9+
let hey = false;
10+
11+
function test() {
12+
hey = "string"
13+
}
14+
15+
if (hey) {}
16+
17+
```

crates/biome_js_type_info/src/resolver.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,14 @@ pub trait TypeResolver {
722722
]))))))
723723
}
724724

725+
/// Register a new type that is a union between `current_type` and `ty`
726+
fn union_with(&mut self, current_type: TypeReference, ty: TypeReference) -> TypeId {
727+
self.register_type(Cow::Owned(TypeData::Union(Box::new(Union(Box::new([
728+
current_type,
729+
ty,
730+
]))))))
731+
}
732+
725733
// #endregion
726734
}
727735

crates/biome_module_graph/src/js_module_info/collector.rs

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ use std::{borrow::Cow, collections::BTreeSet, sync::Arc};
33
use biome_js_semantic::{SemanticEvent, SemanticEventExtractor};
44
use biome_js_syntax::{
55
AnyJsCombinedSpecifier, AnyJsDeclaration, AnyJsExportDefaultDeclaration, AnyJsExpression,
6-
AnyJsImportClause, JsForVariableDeclaration, JsFormalParameter, JsIdentifierBinding,
7-
JsRestParameter, JsSyntaxKind, JsSyntaxNode, JsSyntaxToken, JsVariableDeclaration,
8-
TsIdentifierBinding, TsTypeParameter, TsTypeParameterName, inner_string_text,
6+
AnyJsImportClause, JsAssignmentExpression, JsForVariableDeclaration, JsFormalParameter,
7+
JsIdentifierBinding, JsRestParameter, JsSyntaxKind, JsSyntaxNode, JsSyntaxToken,
8+
JsVariableDeclaration, TsIdentifierBinding, TsTypeParameter, TsTypeParameterName,
9+
inner_string_text,
910
};
1011
use biome_js_type_info::{
1112
BindingId, FunctionParameter, GLOBAL_RESOLVER, GLOBAL_UNKNOWN_ID, GenericTypeParameter,
@@ -573,34 +574,58 @@ impl JsModuleInfoCollector {
573574
for index in 0..self.bindings.len() {
574575
let binding = &self.bindings[index];
575576
if let Some(node) = self.binding_node_by_start.get(&binding.range.start()) {
576-
let name = binding.name.clone();
577577
let scope_id = scope_id_for_range(scope_by_range, binding.range);
578-
let ty = self.infer_type(&node.clone(), &name, scope_id);
578+
let ty = self.infer_type(&node.clone(), binding.clone(), scope_id);
579579
self.bindings[index].ty = ty;
580580
}
581581
}
582582
}
583583

584+
fn has_writable_reference(&self, binding: &JsBindingData) -> bool {
585+
binding
586+
.references
587+
.iter()
588+
.any(|reference| reference.is_write())
589+
}
590+
591+
fn get_writable_references(&self, binding: &JsBindingData) -> Vec<JsBindingReference> {
592+
binding
593+
.references
594+
.iter()
595+
.filter(|reference| reference.is_write())
596+
.cloned()
597+
.collect()
598+
}
599+
584600
fn infer_type(
585601
&mut self,
586602
node: &JsSyntaxNode,
587-
binding_name: &Text,
603+
binding: JsBindingData,
588604
scope_id: ScopeId,
589605
) -> TypeReference {
606+
let binding_name = &binding.name.clone();
590607
for ancestor in node.ancestors() {
591608
if let Some(decl) = AnyJsDeclaration::cast_ref(&ancestor) {
592-
return if let Some(typed_bindings) = decl
609+
let ty = if let Some(typed_bindings) = decl
593610
.as_js_variable_declaration()
594611
.and_then(|decl| self.variable_declarations.get(decl.syntax()))
595612
{
596-
typed_bindings
613+
let ty = typed_bindings
597614
.iter()
598615
.find_map(|(name, ty)| (name == binding_name).then(|| ty.clone()))
599-
.unwrap_or_default()
616+
.unwrap_or_default();
617+
618+
if self.has_writable_reference(&binding) {
619+
self.widen_binding_from_writable_references(scope_id, &binding, &ty)
620+
} else {
621+
ty
622+
}
600623
} else {
601624
let data = TypeData::from_any_js_declaration(self, scope_id, &decl);
602625
self.reference_to_owned_data(data)
603626
};
627+
628+
return ty;
604629
} else if let Some(declaration) = AnyJsExportDefaultDeclaration::cast_ref(&ancestor) {
605630
let data =
606631
TypeData::from_any_js_export_default_declaration(self, scope_id, &declaration);
@@ -640,6 +665,37 @@ impl JsModuleInfoCollector {
640665
TypeReference::unknown()
641666
}
642667

668+
/// Widen the type of binding from its writable references.
669+
fn widen_binding_from_writable_references(
670+
&mut self,
671+
scope_id: ScopeId,
672+
binding: &JsBindingData,
673+
ty: &TypeReference,
674+
) -> TypeReference {
675+
let references = self.get_writable_references(binding);
676+
let mut ty = ty.clone();
677+
for reference in references {
678+
let Some(node) = self.binding_node_by_start.get(&reference.range_start) else {
679+
continue;
680+
};
681+
for ancestor in node.ancestors().skip(1) {
682+
if let Some(assignment) = JsAssignmentExpression::cast_ref(&ancestor)
683+
&& let Ok(right) = assignment.right()
684+
{
685+
let data = TypeData::from_any_js_expression(self, scope_id, &right);
686+
let assigned_type = self.reference_to_owned_data(data);
687+
ty = ResolvedTypeId::new(
688+
self.level(),
689+
self.union_with(ty.clone(), assigned_type),
690+
)
691+
.into();
692+
}
693+
}
694+
}
695+
696+
ty
697+
}
698+
643699
/// After the first pass of the collector, import references have been
644700
/// resolved to an import binding. But we can't store the information of the
645701
/// import target inside the `ResolvedTypeId`, because it resides in the
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
source: crates/biome_module_graph/tests/snap/mod.rs
3+
expression: content
4+
---
5+
# `index.ts` (Not imported by resolver)
6+
7+
## Source
8+
9+
```ts
10+
let hey = false;
11+
function f() {
12+
hey = true;
13+
}
14+
```
15+
16+
## Module Info
17+
18+
```
19+
Exports {
20+
No exports
21+
}
22+
Imports {
23+
No imports
24+
}
25+
```
26+
27+
## Registered types
28+
29+
```
30+
Module TypeId(0) => bool: false
31+
32+
Module TypeId(1) => bool: true
33+
34+
Module TypeId(2) => unknown
35+
36+
Module TypeId(3) => Module(0) TypeId(0) | Module(0) TypeId(1)
37+
38+
Module TypeId(4) => void
39+
40+
Module TypeId(5) => sync Function "f" {
41+
accepts: {
42+
params: []
43+
type_args: []
44+
}
45+
returns: Module(0) TypeId(4)
46+
}
47+
```
48+
49+
# Module Resolver
50+
51+
## Registered types
52+
53+
```
54+
Full TypeId(0) => bool: false
55+
56+
Full TypeId(1) => bool: true
57+
58+
Full TypeId(2) => unknown
59+
60+
Full TypeId(3) => Module(0) TypeId(0) | Module(0) TypeId(1)
61+
62+
Full TypeId(4) => void
63+
64+
Full TypeId(5) => sync Function "f" {
65+
accepts: {
66+
params: []
67+
type_args: []
68+
}
69+
returns: Module(0) TypeId(4)
70+
}
71+
```

0 commit comments

Comments
 (0)