Skip to content

Commit 953f9c6

Browse files
fix(core): resolve issue with rest params in type inference (#7270)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 62fdbc8 commit 953f9c6

20 files changed

+633
-384
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#6172](https://github.com/biomejs/biome/issues/6172): Resolved an issue with inferring types for rest parameters. This issue caused rest-parameter types to be incorrect, and in some cases caused extreme performance regressions in files that contained many methods with rest-parameter definitions.

crates/biome_js_analyze/src/services/typed.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::sync::Arc;
1414
///
1515
/// This service is used for retrieving [`Type`] instances for arbitrary
1616
/// expressions or function definitions from the module graph.
17-
#[derive(Clone, Debug)]
17+
#[derive(Clone)]
1818
pub struct TypedService {
1919
resolver: Option<Arc<ModuleResolver>>,
2020
}

crates/biome_js_type_info/src/local_inference.rs

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ use biome_js_syntax::{
1515
JsFunctionDeclaration, JsFunctionExpression, JsGetterObjectMember, JsInitializerClause,
1616
JsLogicalExpression, JsLogicalOperator, JsMethodObjectMember, JsNewExpression,
1717
JsObjectBindingPattern, JsObjectExpression, JsParameters, JsPropertyClassMember,
18-
JsPropertyObjectMember, JsReferenceIdentifier, JsReturnStatement, JsSetterObjectMember,
19-
JsSyntaxNode, JsSyntaxToken, JsUnaryExpression, JsUnaryOperator, JsVariableDeclaration,
20-
JsVariableDeclarator, TsDeclareFunctionDeclaration, TsExternalModuleDeclaration,
21-
TsInterfaceDeclaration, TsModuleDeclaration, TsReferenceType, TsReturnTypeAnnotation,
22-
TsTypeAliasDeclaration, TsTypeAnnotation, TsTypeArguments, TsTypeList, TsTypeParameter,
23-
TsTypeParameters, TsTypeofType, inner_string_text, unescape_js_string,
18+
JsPropertyObjectMember, JsReferenceIdentifier, JsRestParameter, JsReturnStatement,
19+
JsSetterObjectMember, JsSyntaxNode, JsSyntaxToken, JsUnaryExpression, JsUnaryOperator,
20+
JsVariableDeclaration, JsVariableDeclarator, TsDeclareFunctionDeclaration,
21+
TsExternalModuleDeclaration, TsInterfaceDeclaration, TsModuleDeclaration, TsReferenceType,
22+
TsReturnTypeAnnotation, TsTypeAliasDeclaration, TsTypeAnnotation, TsTypeArguments, TsTypeList,
23+
TsTypeParameter, TsTypeParameters, TsTypeofType, inner_string_text, unescape_js_string,
2424
};
2525
use biome_rowan::{AstNode, SyntaxResult, Text, TokenText};
2626

@@ -833,6 +833,7 @@ impl TypeData {
833833
name,
834834
ty: TypeReference::unknown(),
835835
is_optional: false,
836+
is_rest: false,
836837
})])
837838
}
838839
Ok(AnyJsArrowFunctionParameters::JsParameters(params)) => {
@@ -1507,6 +1508,7 @@ impl FunctionParameter {
15071508
.map(|ty| TypeReference::from_any_ts_type(resolver, scope_id, &ty))
15081509
.unwrap_or_default(),
15091510
is_optional: false,
1511+
is_rest: false,
15101512
}),
15111513
}
15121514
}
@@ -1534,6 +1536,7 @@ impl FunctionParameter {
15341536
name,
15351537
ty: resolver.reference_to_owned_data(ty),
15361538
is_optional: param.question_mark_token().is_some(),
1539+
is_rest: false,
15371540
})
15381541
} else {
15391542
let bindings = param
@@ -1554,6 +1557,50 @@ impl FunctionParameter {
15541557
}
15551558
}
15561559

1560+
pub fn from_js_rest_parameter(
1561+
resolver: &mut dyn TypeResolver,
1562+
scope_id: ScopeId,
1563+
param: &JsRestParameter,
1564+
) -> Self {
1565+
let name = param
1566+
.binding()
1567+
.ok()
1568+
.as_ref()
1569+
.and_then(|binding| binding.as_any_js_binding())
1570+
.and_then(|binding| binding.as_js_identifier_binding())
1571+
.and_then(|identifier| identifier.name_token().ok())
1572+
.map(|token| token.token_text_trimmed().into());
1573+
let ty = param
1574+
.type_annotation()
1575+
.and_then(|annotation| annotation.ty().ok())
1576+
.map(|ty| TypeData::from_any_ts_type(resolver, scope_id, &ty))
1577+
.unwrap_or_default();
1578+
if let Some(name) = name {
1579+
Self::Named(NamedFunctionParameter {
1580+
name,
1581+
ty: resolver.reference_to_owned_data(ty),
1582+
is_optional: false,
1583+
is_rest: true,
1584+
})
1585+
} else {
1586+
let bindings = param
1587+
.binding()
1588+
.ok()
1589+
.and_then(|binding| {
1590+
FunctionParameterBinding::bindings_from_any_js_binding_pattern_of_type(
1591+
resolver, scope_id, &binding, &ty,
1592+
)
1593+
})
1594+
.unwrap_or_default();
1595+
Self::Pattern(PatternFunctionParameter {
1596+
bindings,
1597+
ty: resolver.reference_to_owned_data(ty),
1598+
is_optional: false,
1599+
is_rest: true,
1600+
})
1601+
}
1602+
}
1603+
15571604
pub fn params_from_js_parameters(
15581605
resolver: &mut dyn TypeResolver,
15591606
scope_id: ScopeId,

crates/biome_js_type_info/src/type_data.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,9 @@ pub struct NamedFunctionParameter {
556556

557557
/// Whether the parameter is optional or not.
558558
pub is_optional: bool,
559+
560+
/// Whether this is a rest parameter (`...`) or not.
561+
pub is_rest: bool,
559562
}
560563

561564
/// A function parameter that is bound to either one or more positional

crates/biome_js_type_info/src/type_store.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::{Resolvable, ResolvedTypeId, TypeData, TypeId, TypeReference, TypeRes
2525
/// some amount of deduplication is preferred. The reason for this is that type
2626
/// flattening can reduce disjoint types to the same type, at which point we
2727
/// store both to preserve their indices.
28-
#[derive(Debug, Default)]
28+
#[derive(Default)]
2929
pub struct TypeStore {
3030
types: Vec<Arc<TypeData>>,
3131
table: HashTable<usize>,
@@ -254,9 +254,9 @@ impl TypeStore {
254254
}
255255
}
256256

257-
impl From<TypeStore> for Box<[Arc<TypeData>]> {
257+
impl From<TypeStore> for Vec<Arc<TypeData>> {
258258
fn from(store: TypeStore) -> Self {
259-
store.types.into()
259+
store.types
260260
}
261261
}
262262

crates/biome_module_graph/benches/module_graph.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,10 @@ const INDEX_D_TS_CASES: &[(&str, &[u8])] = &[
3737
"@next/font/google/index.d.ts",
3838
include_bytes!("./next_font_google.d.ts") as &[u8],
3939
),
40-
// FIXME: enable it once the perf reaches a decent number
41-
// (
42-
// "RedisCommander.d.ts",
43-
// include_bytes!("./RedisCommander.d.ts") as &[u8],
44-
// )
40+
(
41+
"RedisCommander.d.ts",
42+
include_bytes!("./RedisCommander.d.ts") as &[u8],
43+
),
4544
];
4645

4746
fn index_d_ts_cases() -> impl Iterator<Item = &'static str> {

crates/biome_module_graph/src/js_module_info.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,24 +174,28 @@ pub struct JsModuleInfoInner {
174174

175175
/// Re-exports that apply to all symbols from another module, without
176176
/// assigning a name to them.
177-
pub blanket_reexports: Box<[JsReexport]>,
177+
pub blanket_reexports: Vec<JsReexport>,
178178

179179
/// Collection of all the declarations in the module.
180-
pub(crate) bindings: Box<[JsBindingData]>,
180+
pub(crate) bindings: Vec<JsBindingData>,
181181

182182
/// Parsed expressions, mapped from their range to their type ID.
183183
pub(crate) expressions: FxHashMap<TextRange, ResolvedTypeId>,
184184

185185
/// All scopes in this module.
186186
///
187187
/// The first entry is expected to be the global scope.
188-
pub(crate) scopes: Box<[JsScopeData]>,
188+
pub(crate) scopes: Vec<JsScopeData>,
189189

190190
/// Lookup tree to find scopes by text range.
191191
pub(crate) scope_by_range: Lapper<u32, ScopeId>,
192192

193193
/// Collection of all types in the module.
194-
pub(crate) types: Box<[Arc<TypeData>]>,
194+
///
195+
/// We do not store these using our `TypeStore`, because once the module
196+
/// info is constructed, no new types can be registered in it, and we have
197+
/// no use for a hash table anymore.
198+
pub(crate) types: Vec<Arc<TypeData>>,
195199
}
196200

197201
#[derive(Debug, Default)]

crates/biome_module_graph/src/js_module_info/collector.rs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use biome_js_semantic::{SemanticEvent, SemanticEventExtractor};
44
use biome_js_syntax::{
55
AnyJsCombinedSpecifier, AnyJsDeclaration, AnyJsExportDefaultDeclaration, AnyJsExpression,
66
AnyJsImportClause, JsForVariableDeclaration, JsFormalParameter, JsIdentifierBinding,
7-
JsSyntaxKind, JsSyntaxNode, JsSyntaxToken, JsVariableDeclaration, TsIdentifierBinding,
8-
TsTypeParameter, TsTypeParameterName, inner_string_text,
7+
JsRestParameter, JsSyntaxKind, JsSyntaxNode, JsSyntaxToken, JsVariableDeclaration,
8+
TsIdentifierBinding, TsTypeParameter, TsTypeParameterName, inner_string_text,
99
};
1010
use biome_js_type_info::{
1111
BindingId, FunctionParameter, GLOBAL_RESOLVER, GLOBAL_UNKNOWN_ID, GenericTypeParameter,
@@ -52,8 +52,8 @@ pub(super) struct JsModuleInfoCollector {
5252
/// Re-used from the semantic model in `biome_js_semantic`.
5353
extractor: SemanticEventExtractor,
5454

55-
/// Formal parameters.
56-
formal_parameters: FxHashMap<JsSyntaxNode, FunctionParameter>,
55+
/// Function parameters, both formal parameters as well as rest parameters.
56+
function_parameters: FxHashMap<JsSyntaxNode, FunctionParameter>,
5757

5858
/// Variable declarations.
5959
variable_declarations: FxHashMap<JsSyntaxNode, Box<[(Text, TypeReference)]>>,
@@ -174,7 +174,12 @@ impl JsModuleInfoCollector {
174174
} else if let Some(param) = JsFormalParameter::cast_ref(node) {
175175
let scope_id = *self.scope_stack.last().expect("there must be a scope");
176176
let parsed_param = FunctionParameter::from_js_formal_parameter(self, scope_id, &param);
177-
self.formal_parameters
177+
self.function_parameters
178+
.insert(param.syntax().clone(), parsed_param);
179+
} else if let Some(param) = JsRestParameter::cast_ref(node) {
180+
let scope_id = *self.scope_stack.last().expect("there must be a scope");
181+
let parsed_param = FunctionParameter::from_js_rest_parameter(self, scope_id, &param);
182+
self.function_parameters
178183
.insert(param.syntax().clone(), parsed_param);
179184
} else if let Some(decl) = JsVariableDeclaration::cast_ref(node) {
180185
let scope_id = *self.scope_stack.last().expect("there must be a scope");
@@ -543,6 +548,10 @@ impl JsModuleInfoCollector {
543548

544549
self.infer_all_types(&scope_by_range);
545550
self.resolve_all_and_downgrade_project_references();
551+
552+
// Purging before flattening will save us from duplicate work during
553+
// flattening. We'll purge again after for a final cleanup.
554+
self.purge_redundant_types();
546555
self.flatten_all();
547556
self.purge_redundant_types();
548557

@@ -553,13 +562,12 @@ impl JsModuleInfoCollector {
553562

554563
fn infer_all_types(&mut self, scope_by_range: &Lapper<u32, ScopeId>) {
555564
for index in 0..self.bindings.len() {
556-
let binding_id = BindingId::new(index);
557-
let binding = &self.bindings[binding_id.index()];
565+
let binding = &self.bindings[index];
558566
if let Some(node) = self.binding_node_by_start.get(&binding.range.start()) {
559567
let name = binding.name.clone();
560568
let scope_id = scope_id_for_range(scope_by_range, binding.range);
561569
let ty = self.infer_type(&node.clone(), &name, scope_id);
562-
self.bindings[binding_id.index()].ty = ty;
570+
self.bindings[index].ty = ty;
563571
}
564572
}
565573
}
@@ -596,7 +604,11 @@ impl JsModuleInfoCollector {
596604
.find_map(|(name, ty)| (name == binding_name).then(|| ty.clone()))
597605
.unwrap_or_default();
598606
} else if let Some(param) = JsFormalParameter::cast_ref(&ancestor)
599-
.and_then(|param| self.formal_parameters.get(param.syntax()))
607+
.and_then(|param| self.function_parameters.get(param.syntax()))
608+
.or_else(|| {
609+
JsRestParameter::cast_ref(&ancestor)
610+
.and_then(|param| self.function_parameters.get(param.syntax()))
611+
})
600612
{
601613
return match param {
602614
FunctionParameter::Named(named) => named.ty.clone(),
@@ -1031,10 +1043,10 @@ impl JsModuleInfo {
10311043
static_import_paths: collector.static_import_paths,
10321044
dynamic_import_paths: collector.dynamic_import_paths,
10331045
exports: Exports(exports),
1034-
blanket_reexports: collector.blanket_reexports.into(),
1035-
bindings: collector.bindings.into(),
1046+
blanket_reexports: collector.blanket_reexports,
1047+
bindings: collector.bindings,
10361048
expressions: collector.parsed_expressions,
1037-
scopes: collector.scopes.into(),
1049+
scopes: collector.scopes,
10381050
scope_by_range,
10391051
types: collector.types.into(),
10401052
}))

crates/biome_module_graph/src/js_module_info/module_resolver.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ const MODULE_0_ID: ResolverId = ResolverId::from_level(TypeResolverLevel::Thin);
4242
/// statements.
4343
///
4444
/// The module resolver is typically consumed through the `Typed` service.
45-
#[derive(Debug)]
4645
pub struct ModuleResolver {
4746
module_graph: Arc<ModuleGraph>,
4847

0 commit comments

Comments
 (0)