diff --git a/.changeset/infinite-indices-infer.md b/.changeset/infinite-indices-infer.md new file mode 100644 index 000000000000..2dc2370a3b7b --- /dev/null +++ b/.changeset/infinite-indices-infer.md @@ -0,0 +1,20 @@ +--- +"@biomejs/biome": patch +--- + +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. + +#### 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_analyze/tests/specs/nursery/noFloatingPromises/03b_invalid.ts b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/03b_invalid.ts new file mode 100644 index 000000000000..0253aa8034d2 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/03b_invalid.ts @@ -0,0 +1,9 @@ +const obj: { [key: string]: () => Promise } = { + asyncFunc, +} + +async function asyncFunc() { + return Promise.resolve("foobar") +} + +obj.asyncFunc() diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/03b_invalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/03b_invalid.ts.snap new file mode 100644 index 000000000000..97d8b3ba9b9d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/03b_invalid.ts.snap @@ -0,0 +1,34 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: 03b_invalid.ts +--- +# Input +```ts +const obj: { [key: string]: () => Promise } = { + asyncFunc, +} + +async function asyncFunc() { + return Promise.resolve("foobar") +} + +obj.asyncFunc() + +``` + +# Diagnostics +``` +03b_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. + + 7 │ } + 8 │ + > 9 │ obj.asyncFunc() + │ ^^^^^^^^^^^^^^^ + 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 3bdded9c163b..9bbb301ae079 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, conditionals::{ ConditionalType, reference_to_falsy_subset_of, reference_to_non_nullish_subset_of, reference_to_truthy_subset_of, @@ -119,81 +119,14 @@ pub(super) fn flattened_expression( }) } } - 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::Index(expr) => { let object = resolver.resolve_and_get(&expr.object)?; - let element_ty = object - .to_data() - .find_element_type_at_index(object.resolver_id(), resolver, expr.index) - .map_or_else(TypeData::unknown, ResolvedTypeData::to_data); - Some(element_ty) + object + .find_element_type_at_index(resolver, expr.index) + .map(|element_reference| element_reference.into_reference(resolver)) + .and_then(|reference| resolver.resolve_and_get(&reference)) + .map(ResolvedTypeData::to_data) } TypeofExpression::IterableValueOf(expr) => { let ty = resolver.resolve_and_get(&expr.ty)?; @@ -325,9 +258,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())) } @@ -356,8 +289,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())) } } @@ -411,8 +346,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() @@ -421,8 +355,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(); @@ -434,6 +367,69 @@ 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) + .map(|element_reference| element_reference.into_reference(resolver)) + .and_then(|reference| 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 7ba82f5f8ba0..4d7a869705c8 100644 --- a/crates/biome_js_type_info/src/format_type_info.rs +++ b/crates/biome_js_type_info/src/format_type_info.rs @@ -349,6 +349,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 77e8b6349b28..226ce58dc91b 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) => 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 { @@ -316,6 +327,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, @@ -580,9 +609,11 @@ 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_undefined, Undefined); generate_matcher!(is_union, Union, _); generate_matcher!(is_unknown_keyword, UnknownKeyword); diff --git a/crates/biome_js_type_info/src/local_inference.rs b/crates/biome_js_type_info/src/local_inference.rs index 9ada89fa34c3..9cff09bf4b93 100644 --- a/crates/biome_js_type_info/src/local_inference.rs +++ b/crates/biome_js_type_info/src/local_inference.rs @@ -2049,29 +2049,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 e884e3ea4dfb..0c39dcafd637 100644 --- a/crates/biome_js_type_info/src/resolver.rs +++ b/crates/biome_js_type_info/src/resolver.rs @@ -479,6 +479,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.rs b/crates/biome_js_type_info/src/type.rs index 5c81dbcee19c..afba56d92720 100644 --- a/crates/biome_js_type_info/src/type.rs +++ b/crates/biome_js_type_info/src/type.rs @@ -178,7 +178,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, @@ -212,7 +212,7 @@ impl Type { } /// Returns whether this type is a string. - pub fn is_string(&self) -> bool { + 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, diff --git a/crates/biome_js_type_info/src/type_data.rs b/crates/biome_js_type_info/src/type_data.rs index ba8a09455a15..321e2b253e04 100644 --- a/crates/biome_js_type_info/src/type_data.rs +++ b/crates/biome_js_type_info/src/type_data.rs @@ -20,7 +20,7 @@ use biome_resolver::ResolvedPath; use biome_rowan::Text; use crate::{ - ModuleId, Resolvable, ResolvedTypeData, ResolvedTypeId, TypeResolver, + ModuleId, Resolvable, ResolvedTypeId, ResolverId, TypeResolver, globals::{GLOBAL_NUMBER_ID, GLOBAL_STRING_ID, GLOBAL_UNKNOWN_ID}, type_data::literal::{BooleanLiteral, NumberLiteral, StringLiteral}, }; @@ -230,6 +230,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)) @@ -846,36 +852,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, - ) -> Option> { - if let Some(elem_type) = self.0.get(index) { - let ty = &elem_type.ty; - if elem_type.is_optional { - let id = resolver.optional(ty.clone()); - resolver.get_by_resolved_id(ResolvedTypeId::new(resolver.level(), id)) - } else { - resolver.resolve_and_get(ty) - } - } else { - let resolved_id = 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) - } + /// 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(), + ) } } @@ -933,6 +931,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() @@ -949,12 +954,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), } @@ -962,7 +968,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 @@ -992,7 +998,7 @@ impl TypeMemberKind { pub fn name(&self) -> Option { match self { - Self::CallSignature => None, + Self::CallSignature | Self::IndexSignature(_) => None, Self::Constructor => Some(Text::new_static("constructor")), Self::Getter(name) | Self::Named(name) | Self::NamedStatic(name) => Some(name.clone()), } diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_country_info.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_country_info.snap index e25585d711ec..4302d360516f 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_country_info.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_country_info.snap @@ -67,10 +67,10 @@ export const codes: { ``` Exports { "CountryInfo" => { - ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(13)) + ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(18)) } "SubdivisionInfo" => { - ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(14)) + ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(19)) } "subdivision" => { ExportOwnExport => JsOwnExport::Binding(12) @@ -95,25 +95,25 @@ Imports { ``` BindingId(12) => JsBindingData { Name: subdivision, - Type: Module(0) TypeId(9), + Type: Module(0) TypeId(14), Declaration kind: HoistedValue } BindingId(15) => JsBindingData { Name: country, - Type: Module(0) TypeId(12), + Type: Module(0) TypeId(17), Declaration kind: HoistedValue } BindingId(17) => JsBindingData { Name: data, - Type: Module(0) TypeId(1), + Type: Module(0) TypeId(0), Declaration kind: Value } BindingId(18) => JsBindingData { Name: codes, - Type: Module(0) TypeId(0), + Type: Module(0) TypeId(1), Declaration kind: Value } ``` @@ -121,37 +121,59 @@ BindingId(18) => JsBindingData { ## Registered types ``` -Module TypeId(0) => Object { +Module TypeId(0) => interface "Map" { + extends: [] + type_args: [] + members: [[string]: Module(0) TypeId(3)] +} + +Module TypeId(1) => Object { prototype: No prototype - members: [] + members: [[string]: string] +} + +Module TypeId(2) => interface "Map" { + extends: [] + type_args: [] + members: [[string]: Module(0) TypeId(7)] } -Module TypeId(1) => interface "Map" { +Module TypeId(3) => Module(0) TypeId(6) | Module(0) TypeId(4) + +Module TypeId(4) => interface "Partial" { extends: [] type_args: [] - members: [] + members: ["name": string, "sub": Module(0) TypeId(2)] } -Module TypeId(2) => interface "Partial" { +Module TypeId(5) => interface "Map" { extends: [] type_args: [] - members: ["name": string, "sub": Module(0) TypeId(1)] + members: [[string]: Module(0) TypeId(3)] } -Module TypeId(3) => interface "Full" { - extends: [Module(0) TypeId(2)] +Module TypeId(6) => interface "Full" { + extends: [Module(0) TypeId(4)] type_args: [] members: ["code": string] } -Module TypeId(4) => interface "Partial" { +Module TypeId(7) => Module(0) TypeId(10) | Module(0) TypeId(8) + +Module TypeId(8) => interface "Partial" { extends: [] type_args: [] members: ["type": string, "name": string] } -Module TypeId(5) => interface "Full" { - extends: [Module(0) TypeId(4)] +Module TypeId(9) => interface "Map" { + extends: [] + type_args: [] + members: [[string]: Module(0) TypeId(7)] +} + +Module TypeId(10) => interface "Full" { + extends: [Module(0) TypeId(8)] type_args: [] members: [ "countryName": string, @@ -161,7 +183,7 @@ Module TypeId(5) => interface "Full" { ] } -Module TypeId(6) => Namespace { +Module TypeId(11) => Namespace { path: Identifier( "SubdivisionInfo", ), @@ -171,7 +193,7 @@ Module TypeId(6) => Namespace { "Partial", ), ty: Resolved( - Module(0) TypeId(4), + Module(0) TypeId(8), ), }, TypeMember { @@ -179,7 +201,7 @@ Module TypeId(6) => Namespace { "Full", ), ty: Resolved( - Module(0) TypeId(5), + Module(0) TypeId(10), ), }, TypeMember { @@ -187,17 +209,17 @@ Module TypeId(6) => Namespace { "Map", ), ty: Resolved( - Module(0) TypeId(1), + Module(0) TypeId(2), ), }, ], } -Module TypeId(7) => null +Module TypeId(12) => null -Module TypeId(8) => Module(0) TypeId(6) | Module(0) TypeId(7) +Module TypeId(13) => Module(0) TypeId(11) | Module(0) TypeId(12) -Module TypeId(9) => sync Function "subdivision" { +Module TypeId(14) => sync Function "subdivision" { accepts: { params: [ required countryCodeOrFullSubdivisionCode: string @@ -205,10 +227,10 @@ Module TypeId(9) => sync Function "subdivision" { ] type_args: [] } - returns: Module(0) TypeId(8) + returns: Module(0) TypeId(13) } -Module TypeId(10) => Namespace { +Module TypeId(15) => Namespace { path: Identifier( "CountryInfo", ), @@ -218,7 +240,7 @@ Module TypeId(10) => Namespace { "Partial", ), ty: Resolved( - Module(0) TypeId(2), + Module(0) TypeId(4), ), }, TypeMember { @@ -226,7 +248,7 @@ Module TypeId(10) => Namespace { "Full", ), ty: Resolved( - Module(0) TypeId(3), + Module(0) TypeId(6), ), }, TypeMember { @@ -234,25 +256,25 @@ Module TypeId(10) => Namespace { "Map", ), ty: Resolved( - Module(0) TypeId(1), + Module(0) TypeId(0), ), }, ], } -Module TypeId(11) => Module(0) TypeId(10) | Module(0) TypeId(7) +Module TypeId(16) => Module(0) TypeId(15) | Module(0) TypeId(12) -Module TypeId(12) => sync Function "country" { +Module TypeId(17) => sync Function "country" { accepts: { params: [ required countryCodeOrName: string ] type_args: [] } - returns: Module(0) TypeId(11) + returns: Module(0) TypeId(16) } -Module TypeId(13) => (type: Module(0) TypeId(3), value: Module(0) TypeId(10), namespace: Module(0) TypeId(10)) +Module TypeId(18) => (type: Module(0) TypeId(6), value: Module(0) TypeId(15), namespace: Module(0) TypeId(15)) -Module TypeId(14) => (type: Module(0) TypeId(5), value: Module(0) TypeId(6), namespace: Module(0) TypeId(6)) +Module TypeId(19) => (type: Module(0) TypeId(10), value: Module(0) TypeId(11), namespace: Module(0) TypeId(11)) ``` diff --git a/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_vfile.snap b/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_vfile.snap index 6615b4e643c4..d6706aaeafe9 100644 --- a/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_vfile.snap +++ b/crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_vfile.snap @@ -178,7 +178,7 @@ Exports { ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(39)) } "data" => { - ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(30)) + ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(16)) } "messages" => { ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(40)) @@ -187,19 +187,19 @@ Exports { ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(35)) } "path" => { - ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(23)) + ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(24)) } "dirname" => { - ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(23)) + ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(24)) } "basename" => { - ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(23)) + ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(24)) } "stem" => { - ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(23)) + ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(24)) } "extname" => { - ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(23)) + ExportOwnExport => JsOwnExport::Type(Module(0) TypeId(24)) } "cwd" => { ExportOwnExport => JsOwnExport::Type(string) @@ -237,7 +237,7 @@ Imports { ## Registered types ``` -Module TypeId(0) => instanceof Module(0) TypeId(27) +Module TypeId(0) => instanceof Module(0) TypeId(28) Module TypeId(1) => Module(0) TypeId(35) | Module(0) TypeId(0) | Module(0) TypeId(37) @@ -247,21 +247,22 @@ Module TypeId(3) => interface "VFile" { extends: [] type_args: [] members: [ - (): Module(0) TypeId(29), + (): Module(0) TypeId(30), "history": Module(0) TypeId(39), - "data": Module(0) TypeId(30), + "data": Module(0) TypeId(16), "messages": Module(0) TypeId(40), "contents": Module(0) TypeId(35), - "path": Module(0) TypeId(23), - "dirname": Module(0) TypeId(23), - "basename": Module(0) TypeId(23), - "stem": Module(0) TypeId(23), - "extname": Module(0) TypeId(23), + "path": Module(0) TypeId(24), + "dirname": Module(0) TypeId(24), + "basename": Module(0) TypeId(24), + "stem": Module(0) TypeId(24), + "extname": Module(0) TypeId(24), "cwd": string, "toString": Module(0) TypeId(31), "message": Module(0) TypeId(32), "fail": Module(0) TypeId(34), - "info": Module(0) TypeId(32) + "info": Module(0) TypeId(32), + [string]: Module(0) TypeId(16) ] } @@ -299,7 +300,7 @@ Module TypeId(4) => Namespace { "Settings", ), ty: Resolved( - Module(0) TypeId(19), + Module(0) TypeId(20), ), }, TypeMember { @@ -307,7 +308,7 @@ Module TypeId(4) => Namespace { "VFileReporter", ), ty: Resolved( - Module(0) TypeId(21), + Module(0) TypeId(22), ), }, TypeMember { @@ -315,7 +316,7 @@ Module TypeId(4) => Namespace { "T", ), ty: Resolved( - Module(0) TypeId(20), + Module(0) TypeId(21), ), }, TypeMember { @@ -359,70 +360,71 @@ Module TypeId(14) => value: hex Module TypeId(15) => Module(0) TypeId(3) | Module(0) TypeId(37) | Module(0) TypeId(35) -Module TypeId(16) => instanceof Array +Module TypeId(16) => unknown + +Module TypeId(17) => instanceof Array -Module TypeId(17) => instanceof Module(0) TypeId(20) +Module TypeId(18) => instanceof Module(0) TypeId(21) -Module TypeId(18) => sync Function { +Module TypeId(19) => sync Function { accepts: { params: [ - required files: Module(0) TypeId(16) - required options: Module(0) TypeId(17) + required files: Module(0) TypeId(17) + required options: Module(0) TypeId(18) ] type_args: [] } returns: string } -Module TypeId(19) => interface "Settings" { +Module TypeId(20) => interface "Settings" { extends: [] type_args: [] - members: [] + members: [[string]: Module(0) TypeId(16)] } -Module TypeId(20) => T = unknown +Module TypeId(21) => T = unknown -Module TypeId(21) => instanceof Module(0) TypeId(18) +Module TypeId(22) => instanceof Module(0) TypeId(19) -Module TypeId(22) => Module(0) TypeId(35) | undefined +Module TypeId(23) => Module(0) TypeId(35) | undefined -Module TypeId(23) => string | undefined +Module TypeId(24) => string | undefined -Module TypeId(24) => any +Module TypeId(25) => any -Module TypeId(25) => Module(0) TypeId(24) | undefined +Module TypeId(26) => Module(0) TypeId(25) | undefined -Module TypeId(26) => interface "VFileOptions" { +Module TypeId(27) => interface "VFileOptions" { extends: [] type_args: [] members: [ - "contents": Module(0) TypeId(22), - "path": Module(0) TypeId(23), - "basename": Module(0) TypeId(23), - "stem": Module(0) TypeId(23), - "extname": Module(0) TypeId(23), - "dirname": Module(0) TypeId(23), - "cwd": Module(0) TypeId(23), - "data": Module(0) TypeId(25) + "contents": Module(0) TypeId(23), + "path": Module(0) TypeId(24), + "basename": Module(0) TypeId(24), + "stem": Module(0) TypeId(24), + "extname": Module(0) TypeId(24), + "dirname": Module(0) TypeId(24), + "cwd": Module(0) TypeId(24), + "data": Module(0) TypeId(26), + [string]: Module(0) TypeId(25) ] } -Module TypeId(27) => F extends Module(0) TypeId(3) +Module TypeId(28) => F extends Module(0) TypeId(3) -Module TypeId(28) => Module(0) TypeId(35) | Module(0) TypeId(36) | Module(0) TypeId(37) +Module TypeId(29) => Module(0) TypeId(35) | Module(0) TypeId(36) | Module(0) TypeId(37) -Module TypeId(29) => sync Function { +Module TypeId(30) => sync Function { accepts: { params: [ - optional input: Module(0) TypeId(28) + optional input: Module(0) TypeId(29) ] - type_args: [Module(0) TypeId(27)] + type_args: [Module(0) TypeId(28)] } returns: Module(0) TypeId(36) } -Module TypeId(30) => unknown - Module TypeId(31) => sync Function { accepts: { params: [ @@ -467,23 +469,24 @@ Module TypeId(37) => interface "VFileOptions" { extends: [] type_args: [] members: [ - "contents": Module(0) TypeId(22), - "path": Module(0) TypeId(23), - "basename": Module(0) TypeId(23), - "stem": Module(0) TypeId(23), - "extname": Module(0) TypeId(23), - "dirname": Module(0) TypeId(23), - "cwd": Module(0) TypeId(23), - "data": Module(0) TypeId(25) + "contents": Module(0) TypeId(23), + "path": Module(0) TypeId(24), + "basename": Module(0) TypeId(24), + "stem": Module(0) TypeId(24), + "extname": Module(0) TypeId(24), + "dirname": Module(0) TypeId(24), + "cwd": Module(0) TypeId(24), + "data": Module(0) TypeId(26), + [string]: Module(0) TypeId(25) ] } Module TypeId(38) => sync Function { accepts: { params: [ - optional input: Module(0) TypeId(28) + optional input: Module(0) TypeId(29) ] - type_args: [Module(0) TypeId(27)] + type_args: [Module(0) TypeId(28)] } returns: Module(0) TypeId(36) } @@ -526,19 +529,20 @@ Module TypeId(45) => interface "VFile" { members: [ (): Module(0) TypeId(38), "history": Module(0) TypeId(39), - "data": Module(0) TypeId(30), + "data": Module(0) TypeId(16), "messages": Module(0) TypeId(40), "contents": Module(0) TypeId(35), - "path": Module(0) TypeId(23), - "dirname": Module(0) TypeId(23), - "basename": Module(0) TypeId(23), - "stem": Module(0) TypeId(23), - "extname": Module(0) TypeId(23), + "path": Module(0) TypeId(24), + "dirname": Module(0) TypeId(24), + "basename": Module(0) TypeId(24), + "stem": Module(0) TypeId(24), + "extname": Module(0) TypeId(24), "cwd": string, "toString": Module(0) TypeId(31), "message": Module(0) TypeId(43), "fail": Module(0) TypeId(44), - "info": Module(0) TypeId(43) + "info": Module(0) TypeId(43), + [string]: Module(0) TypeId(16) ] } ``` diff --git a/crates/biome_module_graph/tests/spec_tests.rs b/crates/biome_module_graph/tests/spec_tests.rs index 8d57bd66c044..68002304a524 100644 --- a/crates/biome_module_graph/tests/spec_tests.rs +++ b/crates/biome_module_graph/tests/spec_tests.rs @@ -692,7 +692,7 @@ fn test_resolve_generic_return_value_with_multiple_modules() { .resolve_type_of(&Text::new_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()); @@ -739,7 +739,7 @@ fn test_resolve_import_as_namespace() { .resolve_type_of(&Text::new_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"); @@ -1618,7 +1618,7 @@ fn test_resolve_single_reexport() { .resolve_type_of(&Text::new_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"); @@ -1748,13 +1748,13 @@ fn test_resolve_multiple_reexports() { .resolve_type_of(&Text::new_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::new_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"); diff --git a/crates/biome_resolver/tests/fixtures/resolver_cases_3/src/components/Foo.ts b/crates/biome_resolver/tests/fixtures/resolver_cases_3/src/components/Foo.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/crates/biome_resolver/tests/fixtures/resolver_cases_3/tsconfig.json b/crates/biome_resolver/tests/fixtures/resolver_cases_3/tsconfig.json new file mode 100644 index 000000000000..cbc1d6838133 --- /dev/null +++ b/crates/biome_resolver/tests/fixtures/resolver_cases_3/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*"] +}