Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/infinite-indices-infer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@biomejs/biome": patch
---

Type inference now recognises _index signatures_ and their accesses when they
are being indexed as a string.

## Example

```ts
type BagOfPromises = {
// This is an index signature definition. It declares that instances of type
// `BagOfPromises` can be indexed using arbitrary strings.
[property: string]: Promise<void>;
};

let bag: BagOfPromises = {};
// Because `bag.iAmAPromise` is equivalent to `bag["iAmAPromise"]`, this is
// considered an access to the string index, and a Promise is expected.
bag.iAmAPromise;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type BagOfPromises = {
[property: string]: Promise<void>;
};

let bag: BagOfPromises = {};
bag.canYouFindMe;

const { anotherOne } = bag;
anotherOne;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: 03_invalid.ts
---
# Input
```ts
type BagOfPromises = {
[property: string]: Promise<void>;
};

let bag: BagOfPromises = {};
bag.canYouFindMe;

const { anotherOne } = bag;
anotherOne;

```

# Diagnostics
```
03_invalid.ts:6:1 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.

5 │ let bag: BagOfPromises = {};
> 6 │ bag.canYouFindMe;
│ ^^^^^^^^^^^^^^^^^
7 │
8 │ const { anotherOne } = bag;

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.


```

```
03_invalid.ts:9:1 lint/nursery/noFloatingPromises ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i A "floating" Promise was found, meaning it is not properly handled and could lead to ignored errors or unexpected behavior.

8 │ const { anotherOne } = bag;
> 9 │ anotherOne;
│ ^^^^^^^^^^^
10 │

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.


```
157 changes: 78 additions & 79 deletions crates/biome_js_type_info/src/flattening/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use rustc_hash::FxHasher;
use crate::{
CallArgumentType, DestructureField, Function, FunctionParameter, Literal, MAX_FLATTEN_DEPTH,
Resolvable, ResolvedTypeData, ResolvedTypeMember, ResolverId, TypeData, TypeMember,
TypeReference, TypeResolver, TypeofCallExpression, TypeofExpression,
TypeofStaticMemberExpression,
TypeReference, TypeResolver, TypeofCallExpression, TypeofDestructureExpression,
TypeofExpression, TypeofStaticMemberExpression,
flattening::conditionals::{
ConditionalType, reference_to_falsy_subset_of, reference_to_non_nullish_subset_of,
reference_to_truthy_subset_of,
Expand Down Expand Up @@ -155,74 +155,7 @@ pub(super) fn flattened_expression(
None
}
}
TypeofExpression::Destructure(expr) => {
let resolved = resolver.resolve_and_get(&expr.ty)?;
match (resolved.as_raw_data(), &expr.destructure_field) {
(_subject, DestructureField::Index(index)) => Some(
resolved
.to_data()
.find_element_type_at_index(resolved.resolver_id(), resolver, *index)
.map_or_else(TypeData::unknown, ResolvedTypeData::to_data),
),
(_subject, DestructureField::RestFrom(index)) => Some(
resolved
.to_data()
.find_type_of_elements_from_index(resolved.resolver_id(), resolver, *index)
.map_or_else(TypeData::unknown, ResolvedTypeData::to_data),
),
(TypeData::InstanceOf(subject_instance), DestructureField::Name(name)) => resolver
.resolve_and_get(&resolved.apply_module_id_to_reference(&subject_instance.ty))
.and_then(|subject| {
subject
.all_members(resolver)
.find(|member| !member.is_static() && member.has_name(name.text()))
})
.and_then(|member| resolver.resolve_and_get(&member.deref_ty(resolver)))
.map(ResolvedTypeData::to_data),
(TypeData::InstanceOf(subject_instance), DestructureField::RestExcept(names)) => {
resolver
.resolve_and_get(
&resolved.apply_module_id_to_reference(&subject_instance.ty),
)
.map(|subject| flattened_rest_object(resolver, subject, names))
}
(subject @ TypeData::Class(_), DestructureField::Name(name)) => {
let member_ty = subject
.own_members()
.find(|own_member| {
own_member.is_static() && own_member.has_name(name.text())
})
.map(|member| resolved.apply_module_id_to_reference(&member.ty))?;
resolver
.resolve_and_get(&member_ty)
.map(ResolvedTypeData::to_data)
}
(subject @ TypeData::Class(_), DestructureField::RestExcept(names)) => {
let members = subject
.own_members()
.filter(|own_member| {
own_member.is_static()
&& !names.iter().any(|name| own_member.has_name(name))
})
.map(|member| {
ResolvedTypeMember::from((resolved.resolver_id(), member)).to_member()
})
.collect();
Some(TypeData::object_with_members(members))
}
(_, DestructureField::Name(name)) => {
let member = resolved
.all_members(resolver)
.find(|member| member.has_name(name.text()))?;
resolver
.resolve_and_get(&member.deref_ty(resolver))
.map(ResolvedTypeData::to_data)
}
(_, DestructureField::RestExcept(excluded_names)) => {
Some(flattened_rest_object(resolver, resolved, excluded_names))
}
}
}
TypeofExpression::Destructure(expr) => flattened_destructure(expr, resolver),
TypeofExpression::New(expr) => {
let resolved = resolver.resolve_and_get(&expr.callee)?;
if let TypeData::Class(class) = resolved.as_raw_data() {
Expand Down Expand Up @@ -297,9 +230,9 @@ pub(super) fn flattened_expression(
let array = resolver
.get_by_resolved_id(GLOBAL_ARRAY_ID)
.expect("Array type must be registered");
let member = array
.all_members(resolver)
.find(|member| member.has_name(&expr.member) && !member.is_static())?;
let member = array.find_member(resolver, |member| {
member.has_name(&expr.member) && !member.is_static()
})?;
Some(TypeData::reference(member.ty().into_owned()))
}

Expand Down Expand Up @@ -330,8 +263,10 @@ pub(super) fn flattened_expression(

_ => {
let member = object
.all_members(resolver)
.find(|member| member.has_name(&expr.member))?;
.find_member(resolver, |member| member.has_name(&expr.member))
.or_else(|| {
object.find_index_signature_with_ty(resolver, |ty| ty.is_string())
})?;
Some(TypeData::reference(member.deref_ty(resolver).into_owned()))
}
}
Expand Down Expand Up @@ -385,8 +320,7 @@ fn flattened_call(
instance_callee.to_data()
} else {
instance_callee
.all_members(resolver)
.find(|member| member.kind().is_call_signature())
.find_member(resolver, |member| member.kind().is_call_signature())
.map(ResolvedTypeMember::to_member)
.and_then(|member| resolver.resolve_and_get(&member.deref_ty(resolver)))?
.to_data()
Expand All @@ -395,8 +329,7 @@ fn flattened_call(
TypeData::Interface(_) | TypeData::Object(_) => {
callee =
ResolvedTypeData::from((ResolverId::from_level(resolver.level()), &callee))
.all_members(resolver)
.find(|member| member.kind().is_call_signature())
.find_member(resolver, |member| member.kind().is_call_signature())
.map(ResolvedTypeMember::to_member)
.and_then(|member| resolver.resolve_and_get(&member.deref_ty(resolver)))?
.to_data();
Expand All @@ -408,6 +341,72 @@ fn flattened_call(
None
}

fn flattened_destructure(
expr: &TypeofDestructureExpression,
resolver: &mut dyn TypeResolver,
) -> Option<TypeData> {
let resolved = resolver.resolve_and_get(&expr.ty)?;
match (resolved.as_raw_data(), &expr.destructure_field) {
(_subject, DestructureField::Index(index)) => resolved
.find_element_type_at_index(resolver, *index)
.and_then(|element_reference| {
let reference = element_reference.into_reference(resolver);
resolver
.resolve_and_get(&reference)
.map(ResolvedTypeData::to_data)
}),
(_subject, DestructureField::RestFrom(index)) => {
resolved.find_type_of_elements_from_index(resolver, *index)
}
(TypeData::InstanceOf(subject_instance), DestructureField::Name(name)) => resolver
.resolve_and_get(&resolved.apply_module_id_to_reference(&subject_instance.ty))
.and_then(|subject| {
subject
.find_member(resolver, |member| {
!member.is_static() && member.has_name(name.text())
})
.or_else(|| subject.find_index_signature_with_ty(resolver, |ty| ty.is_string()))
})
.and_then(|member| resolver.resolve_and_get(&member.deref_ty(resolver)))
.map(ResolvedTypeData::to_data),
(TypeData::InstanceOf(subject_instance), DestructureField::RestExcept(names)) => resolver
.resolve_and_get(&resolved.apply_module_id_to_reference(&subject_instance.ty))
.map(|subject| flattened_rest_object(resolver, subject, names)),
(subject @ TypeData::Class(_), DestructureField::Name(name)) => {
let member_ty = subject
.own_members()
.find(|own_member| own_member.is_static() && own_member.has_name(name.text()))
.map(|member| resolved.apply_module_id_to_reference(&member.ty))?;
resolver
.resolve_and_get(&member_ty)
.map(ResolvedTypeData::to_data)
}
(subject @ TypeData::Class(_), DestructureField::RestExcept(names)) => {
let members = subject
.own_members()
.filter(|own_member| {
own_member.is_static() && !names.iter().any(|name| own_member.has_name(name))
})
.map(|member| {
ResolvedTypeMember::from((resolved.resolver_id(), member)).to_member()
})
.collect();
Some(TypeData::object_with_members(members))
}
(_, DestructureField::Name(name)) => {
let member = resolved
.find_member(resolver, |member| member.has_name(name.text()))
.or_else(|| resolved.find_index_signature_with_ty(resolver, |ty| ty.is_string()))?;
resolver
.resolve_and_get(&member.deref_ty(resolver))
.map(ResolvedTypeData::to_data)
}
(_, DestructureField::RestExcept(excluded_names)) => {
Some(flattened_rest_object(resolver, resolved, excluded_names))
}
}
}

fn flattened_function_call(
expr: &TypeofCallExpression,
function: &Function,
Expand Down
3 changes: 3 additions & 0 deletions crates/biome_js_type_info/src/format_type_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ impl Format<FormatTypeContext> for TypeMemberKind {
let quoted = std::format!("get \"{name}\"");
write!(f, [dynamic_text(&quoted, TextSize::default())])
}
Self::IndexSignature(ty) => {
write!(f, [text("["), ty, text("]")])
}
Self::Named(name) => {
let quoted = std::format!("\"{name}\"");
write!(f, [dynamic_text(&quoted, TextSize::default())])
Expand Down
Loading
Loading