diff --git a/.changeset/infinite-indices-infer.md b/.changeset/infinite-indices-infer.md new file mode 100644 index 000000000000..c2232ea2246e --- /dev/null +++ b/.changeset/infinite-indices-infer.md @@ -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; +}; + +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; +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/03_invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/03_invalid.ts new file mode 100644 index 000000000000..ccd2b44fd2c6 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/03_invalid.ts @@ -0,0 +1,9 @@ +type BagOfPromises = { + [property: string]: Promise; +}; + +let bag: BagOfPromises = {}; +bag.canYouFindMe; + +const { anotherOne } = bag; +anotherOne; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/03_invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/03_invalid.ts.snap new file mode 100644 index 000000000000..9639ce6f5e8f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/03_invalid.ts.snap @@ -0,0 +1,49 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: 03_invalid.ts +--- +# Input +```ts +type BagOfPromises = { + [property: string]: Promise; +}; + +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. + + +``` diff --git a/crates/biome_js_type_info/src/flattening/expressions.rs b/crates/biome_js_type_info/src/flattening/expressions.rs index ada252baa3cd..adabf011b49b 100644 --- a/crates/biome_js_type_info/src/flattening/expressions.rs +++ b/crates/biome_js_type_info/src/flattening/expressions.rs @@ -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, @@ -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() { @@ -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())) } @@ -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())) } } @@ -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() @@ -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(); @@ -408,6 +341,72 @@ fn flattened_call( None } +fn flattened_destructure( + expr: &TypeofDestructureExpression, + resolver: &mut dyn TypeResolver, +) -> Option { + 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, diff --git a/crates/biome_js_type_info/src/format_type_info.rs b/crates/biome_js_type_info/src/format_type_info.rs index 014f81b142ec..6ef09c2f2048 100644 --- a/crates/biome_js_type_info/src/format_type_info.rs +++ b/crates/biome_js_type_info/src/format_type_info.rs @@ -318,6 +318,9 @@ impl Format for TypeMemberKind { let quoted = std::format!("get \"{name}\""); write!(f, [dynamic_text("ed, TextSize::default())]) } + Self::IndexSignature(ty) => { + write!(f, [text("["), ty, text("]")]) + } Self::Named(name) => { let quoted = std::format!("\"{name}\""); write!(f, [dynamic_text("ed, TextSize::default())]) diff --git a/crates/biome_js_type_info/src/helpers.rs b/crates/biome_js_type_info/src/helpers.rs index 3daf893c7c7c..13d1c17e4474 100644 --- a/crates/biome_js_type_info/src/helpers.rs +++ b/crates/biome_js_type_info/src/helpers.rs @@ -42,6 +42,53 @@ impl<'a> ResolvedTypeData<'a> { } } + /// Returns the type of an element at a given index, if this object is an + /// array or a tuple. + pub fn find_element_type_at_index( + self, + resolver: &'a dyn TypeResolver, + index: usize, + ) -> Option { + match self.as_raw_data() { + TypeData::Tuple(tuple) => { + let element = tuple.get_element(index)?; + Some(ElementTypeReference { + ty: self.apply_module_id_to_reference(&element.ty).into_owned(), + is_optional: element.is_optional || element.is_rest, + }) + } + _ if self.is_instance_of(resolver, GLOBAL_ARRAY_ID) => { + self.get_type_parameter(0).map(|ty| ElementTypeReference { + ty: ty.into_owned(), + is_optional: true, + }) + } + _ => None, + } + } + + /// Convenience method for finding a type member of kind index signature. + pub fn find_index_signature_with_ty( + self, + resolver: &'a dyn TypeResolver, + predicate: impl Fn(Self) -> bool, + ) -> Option> { + self.find_member(resolver, |member| { + member.is_index_signature_with_ty(|ty| { + resolver.resolve_and_get(ty).is_some_and(&predicate) + }) + }) + } + + /// Convenience method for `.all_members().find()`. + pub fn find_member( + self, + resolver: &'a dyn TypeResolver, + predicate: impl Fn(&ResolvedTypeMember) -> bool, + ) -> Option> { + self.all_members(resolver).find(predicate) + } + /// Returns the promised type, if this object is an instance of `Promise`. pub fn find_promise_type(self, resolver: &'a dyn TypeResolver) -> Option { if self.is_instance_of(resolver, GLOBAL_PROMISE_ID) { @@ -52,6 +99,30 @@ impl<'a> ResolvedTypeData<'a> { } } + /// Returns the type of elements from a given index, if this object is an + /// array or a tuple. + pub fn find_type_of_elements_from_index( + self, + resolver: &'a dyn TypeResolver, + index: usize, + ) -> Option { + match self.as_raw_data() { + TypeData::Tuple(tuple) => { + Some(TypeData::from(tuple.slice_from(self.resolver_id(), index))) + } + _ if self.is_instance_of(resolver, GLOBAL_ARRAY_ID) => { + match self.get_type_parameter(0) { + Some(elem_ty) => Some(TypeData::instance_of(TypeInstance { + ty: GLOBAL_ARRAY_ID.into(), + type_parameters: [elem_ty.into_owned()].into(), + })), + None => Some(TypeData::instance_of(TypeReference::from(GLOBAL_ARRAY_ID))), + } + } + _ => None, + } + } + pub fn get_type_parameter(self, index: usize) -> Option> { self.as_raw_data() .type_parameters() @@ -154,66 +225,6 @@ impl<'a> ResolvedTypeData<'a> { } impl TypeData { - /// Returns the type of an element at a given index, if this object is an - /// array or a tuple. - pub fn find_element_type_at_index<'a>( - &'a self, - resolver_id: ResolverId, - resolver: &'a mut dyn TypeResolver, - index: usize, - ) -> Option> { - match self { - Self::Tuple(tuple) => Some(tuple.get_ty(resolver, index)), - _ => { - let resolved = ResolvedTypeData::from((resolver_id, self)); - if resolved.is_instance_of(resolver, GLOBAL_ARRAY_ID) { - resolved - .get_type_parameter(0) - .map(|reference| reference.into_owned()) - .map(|reference| resolver.optional(reference)) - .map(|id| { - ResolvedTypeData::from(( - ResolvedTypeId::new(resolver.level(), id), - resolver.get_by_id(id), - )) - }) - } else { - None - } - } - } - } - - /// Returns the type of elements from a given index, if this object is an - /// array or a tuple. - pub fn find_type_of_elements_from_index<'a>( - &'a self, - resolver_id: ResolverId, - resolver: &'a mut dyn TypeResolver, - index: usize, - ) -> Option> { - let data = match self { - Self::Tuple(tuple) => Some(Self::Tuple(Box::new(tuple.slice_from(index)))), - _ => { - let resolved = ResolvedTypeData::from((resolver_id, self)); - if resolved.is_instance_of(resolver, GLOBAL_ARRAY_ID) { - match resolved.get_type_parameter(0) { - Some(elem_ty) => Some(Self::instance_of(TypeInstance { - ty: GLOBAL_ARRAY_ID.into(), - type_parameters: Box::new([elem_ty.into_owned()]), - })), - None => return resolver.get_by_resolved_id(GLOBAL_ARRAY_ID), - } - } else { - None - } - } - }?; - - let id = resolver.register_and_resolve(data); - resolver.get_by_resolved_id(id) - } - /// Turns this [`TypeData`] into an instance of itself. pub fn into_instance(self, resolver: &mut dyn TypeResolver) -> Self { match self { @@ -317,6 +328,24 @@ fn hash_reference(reference: &TypeReference) -> u64 { hash.finish() } +/// A reference to an element that is either optional or not. +pub struct ElementTypeReference { + ty: TypeReference, + is_optional: bool, +} + +impl ElementTypeReference { + pub fn into_reference(self, resolver: &mut dyn TypeResolver) -> TypeReference { + if self.is_optional { + let id = resolver.optional(self.ty); + let resolved_id = ResolvedTypeId::new(resolver.level(), id); + TypeReference::from(resolved_id) + } else { + self.ty + } + } +} + pub struct AllTypeMemberIterator<'a> { resolver: &'a dyn TypeResolver, resolver_id: ResolverId, @@ -581,8 +610,10 @@ generate_matcher!(is_expression, TypeofExpression, _); generate_matcher!(is_function, Function, _); generate_matcher!(is_generic, Generic, _); generate_matcher!(is_interface, Interface, _); +generate_matcher!(is_never_keyword, NeverKeyword); generate_matcher!(is_null, Null); +generate_matcher!(is_number, Number); generate_matcher!(is_reference, Reference, _); -generate_matcher!(is_never_keyword, NeverKeyword); +generate_matcher!(is_string, String); generate_matcher!(is_unknown_keyword, UnknownKeyword); generate_matcher!(is_void_keyword, VoidKeyword); diff --git a/crates/biome_js_type_info/src/local_inference.rs b/crates/biome_js_type_info/src/local_inference.rs index 7b6cdc97a963..072b5dd03363 100644 --- a/crates/biome_js_type_info/src/local_inference.rs +++ b/crates/biome_js_type_info/src/local_inference.rs @@ -1975,29 +1975,41 @@ impl TypeMember { }) } AnyTsTypeMember::TsGetterSignatureTypeMember(member) => { - member.name().ok().and_then(|name| name.name()).map(|name| { - let function = Function { - is_async: false, - type_parameters: [].into(), - name: Some(name.clone().into()), - parameters: [].into(), - return_type: ReturnType::Type(getter_return_type( - resolver, - scope_id, - member.type_annotation(), - None, - )), - }; - let ty = resolver.register_and_resolve(function.into()).into(); - Self { - kind: TypeMemberKind::Getter(name.into()), - ty: ResolvedTypeId::new(resolver.level(), resolver.optional(ty)).into(), - } + let name = member.name().ok().and_then(|name| name.name())?; + let function = Function { + is_async: false, + type_parameters: [].into(), + name: Some(name.clone().into()), + parameters: [].into(), + return_type: ReturnType::Type(getter_return_type( + resolver, + scope_id, + member.type_annotation(), + None, + )), + }; + let ty = resolver.register_and_resolve(function.into()).into(); + Some(Self { + kind: TypeMemberKind::Getter(name.into()), + ty: ResolvedTypeId::new(resolver.level(), resolver.optional(ty)).into(), }) } - AnyTsTypeMember::TsIndexSignatureTypeMember(_member) => { - // TODO: Handle index signatures - None + AnyTsTypeMember::TsIndexSignatureTypeMember(member) => { + let key_ty = member + .parameter() + .and_then(|parameter| parameter.type_annotation()) + .and_then(|annotation| annotation.ty()) + .map(|ty| TypeReference::from_any_ts_type(resolver, scope_id, &ty)) + .ok()?; + let value_ty = member + .type_annotation() + .and_then(|annotation| annotation.ty()) + .map(|ty| TypeReference::from_any_ts_type(resolver, scope_id, &ty)) + .ok()?; + Some(Self { + kind: TypeMemberKind::IndexSignature(key_ty), + ty: value_ty, + }) } AnyTsTypeMember::TsMethodSignatureTypeMember(member) => { member.name().ok().and_then(|name| name.name()).map(|name| { diff --git a/crates/biome_js_type_info/src/resolver.rs b/crates/biome_js_type_info/src/resolver.rs index 7640c068fe9d..120669af1540 100644 --- a/crates/biome_js_type_info/src/resolver.rs +++ b/crates/biome_js_type_info/src/resolver.rs @@ -474,6 +474,12 @@ impl<'a> ResolvedTypeMember<'a> { self.member.is_getter() } + pub fn is_index_signature_with_ty(&self, predicate: impl Fn(&TypeReference) -> bool) -> bool { + self.member.is_index_signature_with_ty(|reference| { + predicate(&self.apply_module_id_to_reference(reference)) + }) + } + #[inline] pub fn is_static(&self) -> bool { self.member.is_static() diff --git a/crates/biome_js_type_info/src/type_info.rs b/crates/biome_js_type_info/src/type_info.rs index 5fbd9447e31e..8b05959eba1e 100644 --- a/crates/biome_js_type_info/src/type_info.rs +++ b/crates/biome_js_type_info/src/type_info.rs @@ -25,7 +25,7 @@ use crate::globals::{ use crate::type_info::literal::{BooleanLiteral, NumberLiteral, StringLiteral}; use crate::{ GLOBAL_RESOLVER, ModuleId, Resolvable, ResolvedTypeData, ResolvedTypeId, ResolvedTypeMember, - TypeResolver, + ResolverId, TypeResolver, }; const UNKNOWN: TypeData = TypeData::Reference(TypeReference::Resolved(GLOBAL_UNKNOWN_ID)); @@ -161,7 +161,7 @@ impl Type { } /// Returns whether this type is a number or a literal number. - pub fn is_number(&self) -> bool { + pub fn is_number_or_number_literal(&self) -> bool { self.id == GLOBAL_NUMBER_ID || self.as_raw_data().is_some_and(|ty| match ty { TypeData::Number => true, @@ -181,8 +181,8 @@ impl Type { .is_some_and(|ty| ty.is_instance_of(self.resolver.as_ref(), GLOBAL_PROMISE_ID)) } - /// Returns whether this type is a string. - pub fn is_string(&self) -> bool { + /// Returns whether this type is a string or a literal string. + pub fn is_string_or_string_literal(&self) -> bool { self.id == GLOBAL_STRING_ID || self.as_raw_data().is_some_and(|ty| match ty { TypeData::String => true, @@ -418,6 +418,12 @@ impl From for TypeData { } } +impl From for TypeData { + fn from(value: Tuple) -> Self { + Self::Tuple(Box::new(value)) + } +} + impl From for TypeData { fn from(value: TypeofExpression) -> Self { Self::TypeofExpression(Box::new(value)) @@ -911,38 +917,28 @@ impl Tuple { &self.0 } - /// Returns the type at the given index. - pub fn get_ty<'a>( - &'a self, - resolver: &'a mut dyn TypeResolver, - index: usize, - ) -> ResolvedTypeData<'a> { - let resolved_id = if let Some(elem_type) = self.0.get(index) { - let ty = elem_type.ty.clone(); - let id = if elem_type.is_optional { - resolver.optional(ty) - } else { - resolver.register_type(Cow::Owned(TypeData::reference(ty))) - }; - ResolvedTypeId::new(resolver.level(), id) - } else { - self.0 - .last() - .filter(|last| last.is_rest) - .map(|last| resolver.optional(last.ty.clone())) - .map_or(GLOBAL_UNKNOWN_ID, |id| { - ResolvedTypeId::new(resolver.level(), id) - }) - }; - - resolver - .get_by_resolved_id(resolved_id) - .expect("tuple element type must be registered") + /// Returns the element at the given index. + pub fn get_element(&self, index: usize) -> Option<&TupleElementType> { + self.0 + .get(index) + .or_else(|| self.0.last().filter(|last| last.is_rest)) } /// Returns a new tuple starting at the given index. - pub fn slice_from(&self, index: usize) -> Self { - Self(self.0.iter().skip(index).cloned().collect()) + pub fn slice_from(&self, resolver_id: ResolverId, index: usize) -> Self { + Self( + self.0 + .iter() + .skip(index) + .map(|element| TupleElementType { + ty: resolver_id + .apply_module_id_to_reference(&element.ty) + .into_owned(), + name: element.name.clone(), + ..*element + }) + .collect(), + ) } } @@ -1000,6 +996,13 @@ impl TypeMember { self.kind.is_constructor() } + pub fn is_index_signature_with_ty(&self, predicate: impl Fn(&TypeReference) -> bool) -> bool { + match &self.kind { + TypeMemberKind::IndexSignature(ty) => predicate(ty), + _ => false, + } + } + #[inline] pub fn is_getter(&self) -> bool { self.kind.is_getter() @@ -1016,12 +1019,13 @@ impl TypeMember { } /// Kind of a [`TypeMember`], with an optional name. -// TODO: Include getters, setters and index signatures. +// TODO: Include setters. #[derive(Clone, Debug, Eq, Hash, PartialEq, Resolvable)] pub enum TypeMemberKind { CallSignature, Constructor, Getter(Text), + IndexSignature(TypeReference), Named(Text), NamedStatic(Text), } @@ -1029,7 +1033,7 @@ pub enum TypeMemberKind { impl TypeMemberKind { pub fn has_name(&self, name: &str) -> bool { match self { - Self::CallSignature => false, + Self::CallSignature | Self::IndexSignature(_) => false, Self::Constructor => name == "constructor", Self::Getter(own_name) | Self::Named(own_name) | Self::NamedStatic(own_name) => { *own_name == name @@ -1059,7 +1063,7 @@ impl TypeMemberKind { pub fn name(&self) -> Option { match self { - Self::CallSignature => None, + Self::CallSignature | Self::IndexSignature(_) => None, Self::Constructor => Some(Text::Static("constructor")), Self::Getter(name) | Self::Named(name) | Self::NamedStatic(name) => Some(name.clone()), } diff --git a/crates/biome_module_graph/tests/spec_tests.rs b/crates/biome_module_graph/tests/spec_tests.rs index acbdb60367f0..34adbf1cfb12 100644 --- a/crates/biome_module_graph/tests/spec_tests.rs +++ b/crates/biome_module_graph/tests/spec_tests.rs @@ -691,7 +691,7 @@ fn test_resolve_generic_return_value_with_multiple_modules() { .resolve_type_of(&Text::Static("result"), ScopeId::GLOBAL) .expect("result variable not found"); let result_ty = resolver.resolved_type_for_id(result_id); - assert!(result_ty.is_string()); + assert!(result_ty.is_string_or_string_literal()); let snapshot = ModuleGraphSnapshot::new(module_graph.as_ref(), &fs).with_resolver(resolver.as_ref()); @@ -738,7 +738,7 @@ fn test_resolve_import_as_namespace() { .resolve_type_of(&Text::Static("result"), ScopeId::GLOBAL) .expect("result variable not found"); let result_ty = resolver.resolved_type_for_id(result_id); - assert!(result_ty.is_number()); + assert!(result_ty.is_number_or_number_literal()); let snapshot = ModuleGraphSnapshot::new(module_graph.as_ref(), &fs).with_resolver(&resolver); snapshot.assert_snapshot("test_resolve_import_as_namespace"); @@ -1309,7 +1309,7 @@ fn test_resolve_single_reexport() { .resolve_type_of(&Text::Static("result"), ScopeId::GLOBAL) .expect("result variable not found"); let ty = resolver.resolved_type_for_id(result_id); - assert!(ty.is_number()); + assert!(ty.is_number_or_number_literal()); let snapshot = ModuleGraphSnapshot::new(module_graph.as_ref(), &fs).with_resolver(&resolver); snapshot.assert_snapshot("test_resolve_single_reexport"); @@ -1374,13 +1374,13 @@ fn test_resolve_multiple_reexports() { .resolve_type_of(&Text::Static("result1"), ScopeId::GLOBAL) .expect("result1 variable not found"); let ty = resolver.resolved_type_for_id(result1_id); - assert!(ty.is_number()); + assert!(ty.is_number_or_number_literal()); let result2_id = resolver .resolve_type_of(&Text::Static("result2"), ScopeId::GLOBAL) .expect("result2 variable not found"); let ty = resolver.resolved_type_for_id(result2_id); - assert!(ty.is_string()); + assert!(ty.is_string_or_string_literal()); let snapshot = ModuleGraphSnapshot::new(module_graph.as_ref(), &fs).with_resolver(&resolver); snapshot.assert_snapshot("test_resolve_multiple_reexports");