Skip to content

Commit 98cf9af

Browse files
authored
feat(core): implement support for index signatures (#6710)
1 parent ac17183 commit 98cf9af

File tree

17 files changed

+512
-302
lines changed

17 files changed

+512
-302
lines changed
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+
Fixed [#4723](https://github.com/biomejs/biome/issues/7423): Type inference now recognises _index signatures_ and their accesses when they are being indexed as a string.
6+
7+
#### Example
8+
9+
```ts
10+
type BagOfPromises = {
11+
// This is an index signature definition. It declares that instances of type
12+
// `BagOfPromises` can be indexed using arbitrary strings.
13+
[property: string]: Promise<void>;
14+
};
15+
16+
let bag: BagOfPromises = {};
17+
// Because `bag.iAmAPromise` is equivalent to `bag["iAmAPromise"]`, this is
18+
// considered an access to the string index, and a Promise is expected.
19+
bag.iAmAPromise;
20+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
type BagOfPromises = {
2+
[property: string]: Promise<void>;
3+
};
4+
5+
let bag: BagOfPromises = {};
6+
bag.canYouFindMe;
7+
8+
const { anotherOne } = bag;
9+
anotherOne;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: 03_invalid.ts
4+
---
5+
# Input
6+
```ts
7+
type BagOfPromises = {
8+
[property: string]: Promise<void>;
9+
};
10+
11+
let bag: BagOfPromises = {};
12+
bag.canYouFindMe;
13+
14+
const { anotherOne } = bag;
15+
anotherOne;
16+
17+
```
18+
19+
# Diagnostics
20+
```
21+
03_invalid.ts:6:1 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22+
23+
i A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.
24+
25+
5 │ let bag: BagOfPromises = {};
26+
> 6 │ bag.canYouFindMe;
27+
│ ^^^^^^^^^^^^^^^^^
28+
7 │
29+
8 │ const { anotherOne } = bag;
30+
31+
i This happens when a Promise is not awaited, lacks a `.catch` or `.then` rejection handler, or is not explicitly ignored using the `void` operator.
32+
33+
34+
```
35+
36+
```
37+
03_invalid.ts:9:1 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
38+
39+
i A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.
40+
41+
8 │ const { anotherOne } = bag;
42+
> 9 │ anotherOne;
43+
│ ^^^^^^^^^^^
44+
10 │
45+
46+
i This happens when a Promise is not awaited, lacks a `.catch` or `.then` rejection handler, or is not explicitly ignored using the `void` operator.
47+
48+
49+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const obj: { [key: string]: () => Promise<string> } = {
2+
asyncFunc,
3+
}
4+
5+
async function asyncFunc() {
6+
return Promise.resolve("foobar")
7+
}
8+
9+
obj.asyncFunc()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: 03b_invalid.ts
4+
---
5+
# Input
6+
```ts
7+
const obj: { [key: string]: () => Promise<string> } = {
8+
asyncFunc,
9+
}
10+
11+
async function asyncFunc() {
12+
return Promise.resolve("foobar")
13+
}
14+
15+
obj.asyncFunc()
16+
17+
```
18+
19+
# Diagnostics
20+
```
21+
03b_invalid.ts:9:1 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22+
23+
i A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.
24+
25+
7 │ }
26+
8 │
27+
> 9 │ obj.asyncFunc()
28+
│ ^^^^^^^^^^^^^^^
29+
10 │
30+
31+
i This happens when a Promise is not awaited, lacks a `.catch` or `.then` rejection handler, or is not explicitly ignored using the `void` operator.
32+
33+
34+
```

crates/biome_js_type_info/src/flattening/expressions.rs

Lines changed: 80 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use rustc_hash::FxHasher;
77
use crate::{
88
CallArgumentType, DestructureField, Function, FunctionParameter, Literal, MAX_FLATTEN_DEPTH,
99
Resolvable, ResolvedTypeData, ResolvedTypeMember, ResolverId, TypeData, TypeMember,
10-
TypeReference, TypeResolver, TypeofCallExpression, TypeofExpression,
11-
TypeofStaticMemberExpression,
10+
TypeReference, TypeResolver, TypeofCallExpression, TypeofDestructureExpression,
11+
TypeofExpression, TypeofStaticMemberExpression,
1212
conditionals::{
1313
ConditionalType, reference_to_falsy_subset_of, reference_to_non_nullish_subset_of,
1414
reference_to_truthy_subset_of,
@@ -119,81 +119,14 @@ pub(super) fn flattened_expression(
119119
})
120120
}
121121
}
122-
TypeofExpression::Destructure(expr) => {
123-
let resolved = resolver.resolve_and_get(&expr.ty)?;
124-
match (resolved.as_raw_data(), &expr.destructure_field) {
125-
(_subject, DestructureField::Index(index)) => Some(
126-
resolved
127-
.to_data()
128-
.find_element_type_at_index(resolved.resolver_id(), resolver, *index)
129-
.map_or_else(TypeData::unknown, ResolvedTypeData::to_data),
130-
),
131-
(_subject, DestructureField::RestFrom(index)) => Some(
132-
resolved
133-
.to_data()
134-
.find_type_of_elements_from_index(resolved.resolver_id(), resolver, *index)
135-
.map_or_else(TypeData::unknown, ResolvedTypeData::to_data),
136-
),
137-
(TypeData::InstanceOf(subject_instance), DestructureField::Name(name)) => resolver
138-
.resolve_and_get(&resolved.apply_module_id_to_reference(&subject_instance.ty))
139-
.and_then(|subject| {
140-
subject
141-
.all_members(resolver)
142-
.find(|member| !member.is_static() && member.has_name(name.text()))
143-
})
144-
.and_then(|member| resolver.resolve_and_get(&member.deref_ty(resolver)))
145-
.map(ResolvedTypeData::to_data),
146-
(TypeData::InstanceOf(subject_instance), DestructureField::RestExcept(names)) => {
147-
resolver
148-
.resolve_and_get(
149-
&resolved.apply_module_id_to_reference(&subject_instance.ty),
150-
)
151-
.map(|subject| flattened_rest_object(resolver, subject, names))
152-
}
153-
(subject @ TypeData::Class(_), DestructureField::Name(name)) => {
154-
let member_ty = subject
155-
.own_members()
156-
.find(|own_member| {
157-
own_member.is_static() && own_member.has_name(name.text())
158-
})
159-
.map(|member| resolved.apply_module_id_to_reference(&member.ty))?;
160-
resolver
161-
.resolve_and_get(&member_ty)
162-
.map(ResolvedTypeData::to_data)
163-
}
164-
(subject @ TypeData::Class(_), DestructureField::RestExcept(names)) => {
165-
let members = subject
166-
.own_members()
167-
.filter(|own_member| {
168-
own_member.is_static()
169-
&& !names.iter().any(|name| own_member.has_name(name))
170-
})
171-
.map(|member| {
172-
ResolvedTypeMember::from((resolved.resolver_id(), member)).to_member()
173-
})
174-
.collect();
175-
Some(TypeData::object_with_members(members))
176-
}
177-
(_, DestructureField::Name(name)) => {
178-
let member = resolved
179-
.all_members(resolver)
180-
.find(|member| member.has_name(name.text()))?;
181-
resolver
182-
.resolve_and_get(&member.deref_ty(resolver))
183-
.map(ResolvedTypeData::to_data)
184-
}
185-
(_, DestructureField::RestExcept(excluded_names)) => {
186-
Some(flattened_rest_object(resolver, resolved, excluded_names))
187-
}
188-
}
189-
}
122+
TypeofExpression::Destructure(expr) => flattened_destructure(expr, resolver),
190123
TypeofExpression::Index(expr) => {
191124
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)
125+
object
126+
.find_element_type_at_index(resolver, expr.index)
127+
.map(|element_reference| element_reference.into_reference(resolver))
128+
.and_then(|reference| resolver.resolve_and_get(&reference))
129+
.map(ResolvedTypeData::to_data)
197130
}
198131
TypeofExpression::IterableValueOf(expr) => {
199132
let ty = resolver.resolve_and_get(&expr.ty)?;
@@ -325,9 +258,9 @@ pub(super) fn flattened_expression(
325258
let array = resolver
326259
.get_by_resolved_id(GLOBAL_ARRAY_ID)
327260
.expect("Array type must be registered");
328-
let member = array
329-
.all_members(resolver)
330-
.find(|member| member.has_name(&expr.member) && !member.is_static())?;
261+
let member = array.find_member(resolver, |member| {
262+
member.has_name(&expr.member) && !member.is_static()
263+
})?;
331264
Some(TypeData::reference(member.ty().into_owned()))
332265
}
333266

@@ -356,8 +289,10 @@ pub(super) fn flattened_expression(
356289

357290
_ => {
358291
let member = object
359-
.all_members(resolver)
360-
.find(|member| member.has_name(&expr.member))?;
292+
.find_member(resolver, |member| member.has_name(&expr.member))
293+
.or_else(|| {
294+
object.find_index_signature_with_ty(resolver, |ty| ty.is_string())
295+
})?;
361296
Some(TypeData::reference(member.deref_ty(resolver).into_owned()))
362297
}
363298
}
@@ -411,8 +346,7 @@ fn flattened_call(
411346
instance_callee.to_data()
412347
} else {
413348
instance_callee
414-
.all_members(resolver)
415-
.find(|member| member.kind().is_call_signature())
349+
.find_member(resolver, |member| member.kind().is_call_signature())
416350
.map(ResolvedTypeMember::to_member)
417351
.and_then(|member| resolver.resolve_and_get(&member.deref_ty(resolver)))?
418352
.to_data()
@@ -421,8 +355,7 @@ fn flattened_call(
421355
TypeData::Interface(_) | TypeData::Object(_) => {
422356
callee =
423357
ResolvedTypeData::from((ResolverId::from_level(resolver.level()), &callee))
424-
.all_members(resolver)
425-
.find(|member| member.kind().is_call_signature())
358+
.find_member(resolver, |member| member.kind().is_call_signature())
426359
.map(ResolvedTypeMember::to_member)
427360
.and_then(|member| resolver.resolve_and_get(&member.deref_ty(resolver)))?
428361
.to_data();
@@ -434,6 +367,69 @@ fn flattened_call(
434367
None
435368
}
436369

370+
fn flattened_destructure(
371+
expr: &TypeofDestructureExpression,
372+
resolver: &mut dyn TypeResolver,
373+
) -> Option<TypeData> {
374+
let resolved = resolver.resolve_and_get(&expr.ty)?;
375+
match (resolved.as_raw_data(), &expr.destructure_field) {
376+
(_subject, DestructureField::Index(index)) => resolved
377+
.find_element_type_at_index(resolver, *index)
378+
.map(|element_reference| element_reference.into_reference(resolver))
379+
.and_then(|reference| resolver.resolve_and_get(&reference))
380+
.map(ResolvedTypeData::to_data),
381+
(_subject, DestructureField::RestFrom(index)) => {
382+
resolved.find_type_of_elements_from_index(resolver, *index)
383+
}
384+
(TypeData::InstanceOf(subject_instance), DestructureField::Name(name)) => resolver
385+
.resolve_and_get(&resolved.apply_module_id_to_reference(&subject_instance.ty))
386+
.and_then(|subject| {
387+
subject
388+
.find_member(resolver, |member| {
389+
!member.is_static() && member.has_name(name.text())
390+
})
391+
.or_else(|| subject.find_index_signature_with_ty(resolver, |ty| ty.is_string()))
392+
})
393+
.and_then(|member| resolver.resolve_and_get(&member.deref_ty(resolver)))
394+
.map(ResolvedTypeData::to_data),
395+
(TypeData::InstanceOf(subject_instance), DestructureField::RestExcept(names)) => resolver
396+
.resolve_and_get(&resolved.apply_module_id_to_reference(&subject_instance.ty))
397+
.map(|subject| flattened_rest_object(resolver, subject, names)),
398+
(subject @ TypeData::Class(_), DestructureField::Name(name)) => {
399+
let member_ty = subject
400+
.own_members()
401+
.find(|own_member| own_member.is_static() && own_member.has_name(name.text()))
402+
.map(|member| resolved.apply_module_id_to_reference(&member.ty))?;
403+
resolver
404+
.resolve_and_get(&member_ty)
405+
.map(ResolvedTypeData::to_data)
406+
}
407+
(subject @ TypeData::Class(_), DestructureField::RestExcept(names)) => {
408+
let members = subject
409+
.own_members()
410+
.filter(|own_member| {
411+
own_member.is_static() && !names.iter().any(|name| own_member.has_name(name))
412+
})
413+
.map(|member| {
414+
ResolvedTypeMember::from((resolved.resolver_id(), member)).to_member()
415+
})
416+
.collect();
417+
Some(TypeData::object_with_members(members))
418+
}
419+
(_, DestructureField::Name(name)) => {
420+
let member = resolved
421+
.find_member(resolver, |member| member.has_name(name.text()))
422+
.or_else(|| resolved.find_index_signature_with_ty(resolver, |ty| ty.is_string()))?;
423+
resolver
424+
.resolve_and_get(&member.deref_ty(resolver))
425+
.map(ResolvedTypeData::to_data)
426+
}
427+
(_, DestructureField::RestExcept(excluded_names)) => {
428+
Some(flattened_rest_object(resolver, resolved, excluded_names))
429+
}
430+
}
431+
}
432+
437433
fn flattened_function_call(
438434
expr: &TypeofCallExpression,
439435
function: &Function,

crates/biome_js_type_info/src/format_type_info.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ impl Format<FormatTypeContext> for TypeMemberKind {
349349
let quoted = std::format!("get \"{name}\"");
350350
write!(f, [dynamic_text(&quoted, TextSize::default())])
351351
}
352+
Self::IndexSignature(ty) => {
353+
write!(f, [text("["), ty, text("]")])
354+
}
352355
Self::Named(name) => {
353356
let quoted = std::format!("\"{name}\"");
354357
write!(f, [dynamic_text(&quoted, TextSize::default())])

0 commit comments

Comments
 (0)