Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/union-static-member-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#8527](https://github.com/biomejs/biome/issues/8527): Improved type inference where analyzing code with repeated object property access and assignments (e.g. `node = node.parent`, a pattern common when traversing trees in a while loop) could hit an internal type limit. Biome now handles these cases without exceeding the type limit.
37 changes: 21 additions & 16 deletions crates/biome_js_type_info/src/flattening/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
CallArgumentType, DestructureField, Function, FunctionParameter, Literal, MAX_FLATTEN_DEPTH,
Resolvable, ResolvedTypeData, ResolvedTypeMember, ResolverId, TypeData, TypeMember,
TypeReference, TypeResolver, TypeofCallExpression, TypeofDestructureExpression,
TypeofExpression, TypeofStaticMemberExpression,
TypeofExpression,
conditionals::{
ConditionalType, reference_to_falsy_subset_of, reference_to_non_nullish_subset_of,
reference_to_truthy_subset_of,
Expand Down Expand Up @@ -265,26 +265,31 @@ pub(super) fn flattened_expression(
}

TypeData::Union(_) => {
let types: Vec<_> = object
// Resolve the requested member across union variants directly and build a union of the resulting references.
let variants: Vec<_> = object
.flattened_union_variants(resolver)
.filter(|variant| *variant != GLOBAL_UNDEFINED_ID.into())
.collect();
let types = types
.into_iter()
.map(|variant| {
// Resolve and flatten the type member for each variant.
let variant = TypeData::TypeofExpression(Box::new(
TypeofExpression::StaticMember(TypeofStaticMemberExpression {
object: variant,
member: expr.member.clone(),
}),
));

resolver.reference_to_owned_data(variant)
})
.collect();
let mut types: Vec<TypeReference> = Vec::new();
for variant in variants {
if let Some(resolved) = resolver.resolve_and_get(&variant) {
let member_opt = resolved
.find_member(resolver, |member| member.has_name(&expr.member))
.or_else(|| {
resolved
.find_index_signature_with_ty(resolver, |ty| ty.is_string())
});
if let Some(member) = member_opt {
let type_ref = resolver.reference_to_owned_data(
TypeData::Reference(member.deref_ty(resolver).into_owned()),
);
types.push(type_ref);
}
}
}

Some(TypeData::union_of(resolver, types))
Some(TypeData::union_of(resolver, types.into_boxed_slice()))
}

_ => {
Expand Down
40 changes: 40 additions & 0 deletions crates/biome_js_type_info/tests/flattening.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,46 @@ use utils::{
parse_ts,
};

#[test]
fn infer_flattened_type_of_static_member_on_union() {
// This test triggers flattening of a static member access on a union type.
// It mimics the original bug where `parentNode = parentNode.parentNode;` caused
// runaway type growth because accessing a property on a union created new
// TypeofExpression::StaticMember nodes for each variant.
const CODE: &str = r#"interface Node {
parentNode: Node | null;
}

declare let parentNode: Node | null;

parentNode.parentNode"#;

let root = parse_ts(CODE);
let interface_decl = get_interface_declaration(&root);
let mut resolver = GlobalsResolver::default();
let interface_ty =
TypeData::from_ts_interface_declaration(&mut resolver, ScopeId::GLOBAL, &interface_decl)
.expect("interface must be inferred");
resolver.run_inference();

// Create the union type: Node | null
let interface_ref = resolver.reference_to_owned_data(interface_ty.clone());
let null_ref = resolver.reference_to_owned_data(TypeData::Null);
let union_ty = TypeData::union_of(&resolver, [interface_ref, null_ref].into());

let expr = get_expression(&root);
let mut resolver = HardcodedSymbolResolver::new("parentNode", union_ty, resolver);
let expr_ty = TypeData::from_any_js_expression(&mut resolver, ScopeId::GLOBAL, &expr);
let expr_ty = expr_ty.inferred(&mut resolver);

assert_type_data_snapshot(
CODE,
&expr_ty,
&resolver,
"infer_flattened_type_of_static_member_on_union",
)
}

#[test]
fn infer_flattened_type_of_typeof_expression() {
const CODE: &str = r#"const foo = "foo";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
source: crates/biome_js_type_info/tests/utils.rs
expression: content
---
## Input

```ts
interface Node {
parentNode: Node | null;
}

declare let parentNode: Node | null;

parentNode.parentNode;

```

## Result

```
Global TypeId(0) | Global TypeId(1) | Global TypeId(2)
```

## Registered types

```
Thin TypeId(0) => Global TypeId(3) | Global TypeId(1)

Global TypeId(0) => instanceof unresolved reference "Node" (scope ID: 0)

Global TypeId(1) => null

Global TypeId(2) => Global TypeId(0) | Global TypeId(1)

Global TypeId(3) => interface "Node" {
extends: []
type_args: []
members: ["parentNode": Global TypeId(2)]
}
```
Loading