Skip to content

Commit 5beb024

Browse files
authored
fix(core): infer array indices (#6898)
1 parent 533956a commit 5beb024

File tree

9 files changed

+212
-83
lines changed

9 files changed

+212
-83
lines changed

.changeset/indexed-items-infer.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#6891](https://github.com/biomejs/biome/issues/6891): Improved type inference for array indices.
6+
7+
**Example:**
8+
9+
```ts
10+
const numbers: number[];
11+
numbers[42] // This now infers to `number | undefined`.
12+
```
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
type State = "running" | "jumping" | "ducking"
2+
3+
class Player {
4+
state: State
5+
constructor(state: State) {
6+
this.state = state
7+
}
8+
}
9+
10+
export function updatePlayers(players: Player[]) {
11+
switch(players[0].state) {
12+
case "running":
13+
break;
14+
// Here Biome should error out saying that the "jumping" and "ducking" cases are not handled
15+
}
16+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalidIssue6891.ts
4+
---
5+
# Input
6+
```ts
7+
type State = "running" | "jumping" | "ducking"
8+
9+
class Player {
10+
state: State
11+
constructor(state: State) {
12+
this.state = state
13+
}
14+
}
15+
16+
export function updatePlayers(players: Player[]) {
17+
switch(players[0].state) {
18+
case "running":
19+
break;
20+
// Here Biome should error out saying that the "jumping" and "ducking" cases are not handled
21+
}
22+
}
23+
24+
```
25+
26+
# Diagnostics
27+
```
28+
invalidIssue6891.ts:11:3 lint/nursery/useExhaustiveSwitchCases FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━
29+
30+
i The switch statement is not exhaustive.
31+
32+
10 │ export function updatePlayers(players: Player[]) {
33+
> 11switch(players[0].state) {
34+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
35+
> 12 │ case "running":
36+
> 13break;
37+
> 14// Here Biome should error out saying that the "jumping" and "ducking" cases are not handled
38+
> 15 │ }
39+
^
40+
16}
41+
17 │
42+
43+
i Some variants of the union type are not handled here.
44+
45+
i These cases are missing:
46+
47+
- "jumping"
48+
- "ducking"
49+
50+
i Unsafe fix: Add the missing cases to the switch statement.
51+
52+
11 11 │ switch(players[0].state) {
53+
12 12case "running":
54+
13- ······break;
55+
13+ ······break;
56+
14+ ····case·"jumping"throw·new·Error("TODO:·Not·implemented·yet");
57+
15+ ····case·"ducking"throw·new·Error("TODO:·Not·implemented·yet");
58+
14 16// Here Biome should error out saying that the "jumping" and "ducking" cases are not handled
59+
15 17}
60+
61+
62+
```

crates/biome_js_type_info/src/flattening/expressions.rs

Lines changed: 71 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
GLOBAL_FUNCTION_STRING_LITERAL_ID, GLOBAL_NUMBER_STRING_LITERAL_ID,
1919
GLOBAL_OBJECT_STRING_LITERAL_ID, GLOBAL_STRING_STRING_LITERAL_ID,
2020
GLOBAL_SYMBOL_STRING_LITERAL_ID, GLOBAL_TYPEOF_OPERATOR_RETURN_UNION_ID,
21-
GLOBAL_UNDEFINED_STRING_LITERAL_ID,
21+
GLOBAL_UNDEFINED_ID, GLOBAL_UNDEFINED_STRING_LITERAL_ID,
2222
},
2323
};
2424

@@ -119,62 +119,6 @@ pub(super) fn flattened_expression(
119119
})
120120
}
121121
}
122-
TypeofExpression::IterableValueOf(expr) => {
123-
let ty = resolver.resolve_and_get(&expr.ty)?;
124-
match ty.as_raw_data() {
125-
TypeData::InstanceOf(instance)
126-
if instance.ty == GLOBAL_ARRAY_ID.into()
127-
&& instance.has_known_type_parameters() =>
128-
{
129-
instance
130-
.type_parameters
131-
.first()
132-
.map(|param| ty.apply_module_id_to_reference(param))
133-
.and_then(|param| resolver.resolve_and_get(&param))
134-
.map(ResolvedTypeData::to_data)
135-
}
136-
_ => {
137-
// TODO: Handle other iterable types
138-
None
139-
}
140-
}
141-
}
142-
TypeofExpression::LogicalAnd(expr) => {
143-
let left = resolver.resolve_and_get(&expr.left)?;
144-
let conditional = ConditionalType::from_resolved_data(left, resolver);
145-
if conditional.is_falsy() {
146-
Some(left.to_data())
147-
} else if conditional.is_truthy() {
148-
Some(TypeData::reference(expr.right.clone()))
149-
} else if conditional.is_inferred() {
150-
let left = reference_to_falsy_subset_of(&left.to_data(), resolver)
151-
.unwrap_or_else(|| expr.left.clone());
152-
Some(TypeData::union_of(
153-
resolver,
154-
[left, expr.right.clone()].into(),
155-
))
156-
} else {
157-
None
158-
}
159-
}
160-
TypeofExpression::LogicalOr(expr) => {
161-
let left = resolver.resolve_and_get(&expr.left)?;
162-
let conditional = ConditionalType::from_resolved_data(left, resolver);
163-
if conditional.is_truthy() {
164-
Some(left.to_data())
165-
} else if conditional.is_falsy() {
166-
Some(TypeData::reference(expr.right.clone()))
167-
} else if conditional.is_inferred() {
168-
let left = reference_to_truthy_subset_of(&left.to_data(), resolver)
169-
.unwrap_or_else(|| expr.left.clone());
170-
Some(TypeData::union_of(
171-
resolver,
172-
[left, expr.right.clone()].into(),
173-
))
174-
} else {
175-
None
176-
}
177-
}
178122
TypeofExpression::Destructure(expr) => {
179123
let resolved = resolver.resolve_and_get(&expr.ty)?;
180124
match (resolved.as_raw_data(), &expr.destructure_field) {
@@ -243,6 +187,70 @@ pub(super) fn flattened_expression(
243187
}
244188
}
245189
}
190+
TypeofExpression::Index(expr) => {
191+
let object = resolver.resolve_and_get(&expr.object)?;
192+
let element_ty = object
193+
.to_data()
194+
.find_element_type_at_index(object.resolver_id(), resolver, expr.index)
195+
.map_or_else(TypeData::unknown, ResolvedTypeData::to_data);
196+
Some(element_ty)
197+
}
198+
TypeofExpression::IterableValueOf(expr) => {
199+
let ty = resolver.resolve_and_get(&expr.ty)?;
200+
match ty.as_raw_data() {
201+
TypeData::InstanceOf(instance)
202+
if instance.ty == GLOBAL_ARRAY_ID.into()
203+
&& instance.has_known_type_parameters() =>
204+
{
205+
instance
206+
.type_parameters
207+
.first()
208+
.map(|param| ty.apply_module_id_to_reference(param))
209+
.and_then(|param| resolver.resolve_and_get(&param))
210+
.map(ResolvedTypeData::to_data)
211+
}
212+
_ => {
213+
// TODO: Handle other iterable types
214+
None
215+
}
216+
}
217+
}
218+
TypeofExpression::LogicalAnd(expr) => {
219+
let left = resolver.resolve_and_get(&expr.left)?;
220+
let conditional = ConditionalType::from_resolved_data(left, resolver);
221+
if conditional.is_falsy() {
222+
Some(left.to_data())
223+
} else if conditional.is_truthy() {
224+
Some(TypeData::reference(expr.right.clone()))
225+
} else if conditional.is_inferred() {
226+
let left = reference_to_falsy_subset_of(&left.to_data(), resolver)
227+
.unwrap_or_else(|| expr.left.clone());
228+
Some(TypeData::union_of(
229+
resolver,
230+
[left, expr.right.clone()].into(),
231+
))
232+
} else {
233+
None
234+
}
235+
}
236+
TypeofExpression::LogicalOr(expr) => {
237+
let left = resolver.resolve_and_get(&expr.left)?;
238+
let conditional = ConditionalType::from_resolved_data(left, resolver);
239+
if conditional.is_truthy() {
240+
Some(left.to_data())
241+
} else if conditional.is_falsy() {
242+
Some(TypeData::reference(expr.right.clone()))
243+
} else if conditional.is_inferred() {
244+
let left = reference_to_truthy_subset_of(&left.to_data(), resolver)
245+
.unwrap_or_else(|| expr.left.clone());
246+
Some(TypeData::union_of(
247+
resolver,
248+
[left, expr.right.clone()].into(),
249+
))
250+
} else {
251+
None
252+
}
253+
}
246254
TypeofExpression::New(expr) => {
247255
let resolved = resolver.resolve_and_get(&expr.callee)?;
248256
if let TypeData::Class(class) = resolved.as_raw_data() {
@@ -332,7 +340,11 @@ pub(super) fn flattened_expression(
332340
.collect();
333341
let types = types
334342
.into_iter()
335-
.map(|variant| {
343+
.filter_map(|variant| {
344+
if variant == GLOBAL_UNDEFINED_ID.into() {
345+
return None;
346+
}
347+
336348
// Resolve and flatten the type member for each variant.
337349
let variant = TypeData::TypeofExpression(Box::new(
338350
TypeofExpression::StaticMember(TypeofStaticMemberExpression {
@@ -341,7 +353,7 @@ pub(super) fn flattened_expression(
341353
}),
342354
));
343355

344-
resolver.reference_to_owned_data(variant)
356+
Some(resolver.reference_to_owned_data(variant))
345357
})
346358
.collect();
347359

crates/biome_js_type_info/src/format_type_info.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,15 @@ impl Format<FormatTypeContext> for TypeofExpression {
474474
)
475475
}
476476
},
477+
Self::Index(expr) => {
478+
write!(
479+
f,
480+
[&format_args![
481+
&expr.object,
482+
dynamic_text(&std::format!("[{}]", expr.index), TextSize::default()),
483+
]]
484+
)
485+
}
477486
Self::IterableValueOf(expr) => {
478487
write!(
479488
f,

crates/biome_js_type_info/src/helpers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ impl TypeData {
163163
index: usize,
164164
) -> Option<ResolvedTypeData<'a>> {
165165
match self {
166-
Self::Tuple(tuple) => Some(tuple.get_ty(resolver, index)),
166+
Self::Tuple(tuple) => tuple.get_ty(resolver, index),
167167
_ => {
168168
let resolved = ResolvedTypeData::from((resolver_id, self));
169169
if resolved.is_instance_of(resolver, GLOBAL_ARRAY_ID) {

crates/biome_js_type_info/src/local_inference.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ use crate::{
3636
TypeOperatorType, TypeReference, TypeReferenceQualifier, TypeResolver,
3737
TypeofAdditionExpression, TypeofAwaitExpression, TypeofBitwiseNotExpression,
3838
TypeofCallExpression, TypeofConditionalExpression, TypeofDestructureExpression,
39-
TypeofExpression, TypeofIterableValueOfExpression, TypeofLogicalAndExpression,
40-
TypeofLogicalOrExpression, TypeofNewExpression, TypeofNullishCoalescingExpression,
41-
TypeofStaticMemberExpression, TypeofThisOrSuperExpression, TypeofTypeofExpression,
42-
TypeofUnaryMinusExpression, TypeofValue,
39+
TypeofExpression, TypeofIndexExpression, TypeofIterableValueOfExpression,
40+
TypeofLogicalAndExpression, TypeofLogicalOrExpression, TypeofNewExpression,
41+
TypeofNullishCoalescingExpression, TypeofStaticMemberExpression, TypeofThisOrSuperExpression,
42+
TypeofTypeofExpression, TypeofUnaryMinusExpression, TypeofValue,
4343
};
4444

4545
impl TypeData {
@@ -497,6 +497,23 @@ impl TypeData {
497497
))
498498
})
499499
.unwrap_or_default(),
500+
(
501+
Ok(object),
502+
Ok(AnyJsExpression::AnyJsLiteralExpression(
503+
AnyJsLiteralExpression::JsNumberLiteralExpression(member),
504+
)),
505+
) => unescaped_text_from_token(member.value_token())
506+
.map(|member| match member.parse() {
507+
Ok(index) => {
508+
Self::from(TypeofExpression::Index(TypeofIndexExpression {
509+
object: resolver
510+
.reference_to_resolved_expression(scope_id, &object),
511+
index,
512+
}))
513+
}
514+
Err(_) => Self::unknown(),
515+
})
516+
.unwrap_or_default(),
500517
_ => Self::unknown(),
501518
}
502519
}

crates/biome_js_type_info/src/type_info.rs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -944,28 +944,26 @@ impl Tuple {
944944
&'a self,
945945
resolver: &'a mut dyn TypeResolver,
946946
index: usize,
947-
) -> ResolvedTypeData<'a> {
948-
let resolved_id = if let Some(elem_type) = self.0.get(index) {
949-
let ty = elem_type.ty.clone();
950-
let id = if elem_type.is_optional {
951-
resolver.optional(ty)
947+
) -> Option<ResolvedTypeData<'a>> {
948+
if let Some(elem_type) = self.0.get(index) {
949+
let ty = &elem_type.ty;
950+
if elem_type.is_optional {
951+
let id = resolver.optional(ty.clone());
952+
resolver.get_by_resolved_id(ResolvedTypeId::new(resolver.level(), id))
952953
} else {
953-
resolver.register_type(Cow::Owned(TypeData::reference(ty)))
954-
};
955-
ResolvedTypeId::new(resolver.level(), id)
954+
resolver.resolve_and_get(ty)
955+
}
956956
} else {
957-
self.0
957+
let resolved_id = self
958+
.0
958959
.last()
959960
.filter(|last| last.is_rest)
960961
.map(|last| resolver.optional(last.ty.clone()))
961962
.map_or(GLOBAL_UNKNOWN_ID, |id| {
962963
ResolvedTypeId::new(resolver.level(), id)
963-
})
964-
};
965-
966-
resolver
967-
.get_by_resolved_id(resolved_id)
968-
.expect("tuple element type must be registered")
964+
});
965+
resolver.get_by_resolved_id(resolved_id)
966+
}
969967
}
970968

971969
/// Returns a new tuple starting at the given index.
@@ -1129,6 +1127,7 @@ pub enum TypeofExpression {
11291127
Call(TypeofCallExpression),
11301128
Conditional(TypeofConditionalExpression),
11311129
Destructure(TypeofDestructureExpression),
1130+
Index(TypeofIndexExpression),
11321131
IterableValueOf(TypeofIterableValueOfExpression),
11331132
LogicalAnd(TypeofLogicalAndExpression),
11341133
LogicalOr(TypeofLogicalOrExpression),
@@ -1218,6 +1217,12 @@ pub enum CallArgumentType {
12181217
Spread(TypeReference),
12191218
}
12201219

1220+
#[derive(Clone, Debug, Eq, Hash, PartialEq, Resolvable)]
1221+
pub struct TypeofIndexExpression {
1222+
pub object: TypeReference,
1223+
pub index: usize,
1224+
}
1225+
12211226
#[derive(Clone, Debug, Eq, Hash, PartialEq, Resolvable)]
12221227
pub struct TypeofNullishCoalescingExpression {
12231228
pub left: TypeReference,

crates/biome_module_graph/tests/snapshots/test_resolve_exports.snap

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,6 @@ Module TypeId(17) => sync Function "Component" {
291291
}
292292
returns: Module(0) TypeId(16)
293293
}
294-
295-
Module TypeId(18) => Module(0) TypeId(7)
296-
297-
Module TypeId(19) => Module(0) TypeId(8)
298294
```
299295
300296
# `/src/renamed-reexports.ts`

0 commit comments

Comments
 (0)