diff --git a/CHANGELOG.md b/CHANGELOG.md index c59971b14b3b..572b02f80e04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom ### JavaScript APIs ### Linter +#### Bug fixes + +- Fix [#243](https://github.com/biomejs/biome/issues/243) a false positive case where the incorrect scope was defined for the `infer` type. in rule [noUndeclaredVariables](https://biomejs.dev/linter/rules/no-undeclared-variables/). Contributed by @denbezrukov + #### New features - Add [noMisleadingInstantiator](https://biomejs.dev/linter/rules/no-mileading-instantiator) rule. The rule reports the misleading use of the `new` and `constructor` methods. Contributed by @unvalley diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer.ts b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer.ts new file mode 100644 index 000000000000..c1b7b6ad1058 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer.ts @@ -0,0 +1,5 @@ +export type WithSelectors = S extends { getState: () => infer T } + ? S & { use: { [K in keyof T]: () => T[K] } } + : never; + +type A = number extends infer T ? T : never; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer.ts.snap b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer.ts.snap new file mode 100644 index 000000000000..dfb0625c71c9 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer.ts.snap @@ -0,0 +1,15 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: infer.ts +--- +# Input +```js +export type WithSelectors = S extends { getState: () => infer T } + ? S & { use: { [K in keyof T]: () => T[K] } } + : never; + +type A = number extends infer T ? T : never; + +``` + + diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer_incorrect.ts b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer_incorrect.ts new file mode 100644 index 000000000000..d0f0b6958f7f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer_incorrect.ts @@ -0,0 +1 @@ +type A = number extends infer T ? never : T; diff --git a/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer_incorrect.ts.snap b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer_incorrect.ts.snap new file mode 100644 index 000000000000..4d5989e750b1 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/correctness/noUndeclaredVariables/infer_incorrect.ts.snap @@ -0,0 +1,24 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: infer_incorrect.ts +--- +# Input +```js +type A = number extends infer T ? never : T; + +``` + +# Diagnostics +``` +infer_incorrect.ts:1:43 lint/correctness/noUndeclaredVariables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! The T variable is undeclared + + > 1 │ type A = number extends infer T ? never : T; + │ ^ + 2 │ + + +``` + + diff --git a/crates/biome_js_semantic/src/events.rs b/crates/biome_js_semantic/src/events.rs index 1dce13b58b97..844eb92147bd 100644 --- a/crates/biome_js_semantic/src/events.rs +++ b/crates/biome_js_semantic/src/events.rs @@ -2,7 +2,9 @@ use rustc_hash::FxHashMap; use std::collections::{HashMap, VecDeque}; +use std::mem; +use biome_js_syntax::AnyTsType; use biome_js_syntax::{ AnyJsAssignment, AnyJsAssignmentPattern, AnyJsExpression, JsAssignmentExpression, JsCallExpression, JsForVariableDeclaration, JsIdentifierAssignment, JsIdentifierBinding, @@ -162,6 +164,7 @@ pub struct SemanticEventExtractor { /// At any point this is the set of available bindings and /// the range of its declaration bindings: FxHashMap, + infers: Vec, } /// Holds the text range of the token when it is bound, @@ -247,6 +250,7 @@ impl SemanticEventExtractor { scopes: vec![], next_scope_id: 0, bindings: FxHashMap::default(), + infers: vec![], } } @@ -304,7 +308,6 @@ impl SemanticEventExtractor { | TS_INTERFACE_DECLARATION | TS_ENUM_DECLARATION | TS_TYPE_ALIAS_DECLARATION - | TS_FUNCTION_TYPE | TS_DECLARE_FUNCTION_DECLARATION => { self.push_scope( node.text_range(), @@ -321,8 +324,11 @@ impl SemanticEventExtractor { false, ); } - - _ => {} + _ => { + if let Some(node) = AnyTsType::cast_ref(node) { + self.enter_any_type(&node); + } + } } } @@ -348,6 +354,18 @@ impl SemanticEventExtractor { Some(is_var) } + fn enter_any_type(&mut self, node: &AnyTsType) { + if node.in_conditional_true_type() { + self.push_conditional_true_scope(node); + } else if let Some(node) = node.as_ts_function_type() { + self.push_scope( + node.syntax().text_range(), + ScopeHoisting::DontHoistDeclarationsToParent, + false, + ); + } + } + fn enter_identifier_binding(&mut self, node: &JsSyntaxNode) -> Option<()> { use JsSyntaxKind::*; debug_assert!(matches!( @@ -371,6 +389,13 @@ impl SemanticEventExtractor { TS_TYPE_PARAMETER_NAME => { let token = node.clone().cast::()?; let name_token = token.ident_token().ok()?; + + if token.in_infer_type() { + self.infers.push(token); + + return None; + } + let is_var = Some(false); (name_token, is_var) } @@ -562,7 +587,6 @@ impl SemanticEventExtractor { | JS_CATCH_CLAUSE | JS_STATIC_INITIALIZATION_BLOCK_CLASS_MEMBER | TS_DECLARE_FUNCTION_DECLARATION - | TS_FUNCTION_TYPE | TS_INTERFACE_DECLARATION | TS_ENUM_DECLARATION | TS_TYPE_ALIAS_DECLARATION @@ -570,7 +594,19 @@ impl SemanticEventExtractor { | TS_EXTERNAL_MODULE_DECLARATION => { self.pop_scope(node.text_range()); } - _ => {} + _ => { + if let Some(node) = AnyTsType::cast_ref(node) { + self.leave_any_type(&node); + } + } + } + } + + fn leave_any_type(&mut self, node: &AnyTsType) { + if node.in_conditional_true_type() { + self.pop_scope(node.syntax().text_range()); + } else if let Some(node) = node.as_ts_function_type() { + self.pop_scope(node.syntax().text_range()); } } @@ -580,6 +616,24 @@ impl SemanticEventExtractor { self.stash.pop_front() } + fn push_conditional_true_scope(&mut self, node: &AnyTsType) { + self.push_scope( + node.syntax().text_range(), + ScopeHoisting::DontHoistDeclarationsToParent, + false, + ); + + let infers = mem::take(&mut self.infers); + for infer in infers { + let name_token = infer.ident_token().ok(); + let parent_kind = infer.syntax().parent().map(|parent| parent.kind()); + + if let (Some(name_token), Some(parent_kind)) = (name_token, parent_kind) { + self.push_binding_into_scope(None, &name_token, &parent_kind); + } + } + } + fn push_scope(&mut self, range: TextRange, hoisting: ScopeHoisting, is_closure: bool) { let scope_id = self.next_scope_id; self.next_scope_id += 1; diff --git a/crates/biome_js_semantic/src/semantic_model/scope.rs b/crates/biome_js_semantic/src/semantic_model/scope.rs index 606d5c059cff..9052cd2aae61 100644 --- a/crates/biome_js_semantic/src/semantic_model/scope.rs +++ b/crates/biome_js_semantic/src/semantic_model/scope.rs @@ -14,7 +14,7 @@ pub(crate) struct SemanticModelScopeData { pub(crate) children: Vec, // All bindings of this scope (points to SemanticModelData::bindings) pub(crate) bindings: Vec, - // Map pointing to the [bindings] vec of each bindings by its name + // Map pointing to the [bindings] vec of each bindings by its name pub(crate) bindings_by_name: FxHashMap, // All read references of a scope pub(crate) read_references: Vec, diff --git a/crates/biome_js_semantic/src/tests/infer.rs b/crates/biome_js_semantic/src/tests/infer.rs new file mode 100644 index 000000000000..b04eeed50dac --- /dev/null +++ b/crates/biome_js_semantic/src/tests/infer.rs @@ -0,0 +1,8 @@ +use crate::assert_semantics; + +assert_semantics! { + ok_conditional_true_type_scope, "type A = T extends string ? (/*START Scope*/number)/*END Scope*/: boolean;", + ok_conditional_true_type_infer_simple, "type A = T extends infer /*@ Scope */T ? (/*START Scope*/number)/*END Scope*/: boolean;", + ok_conditional_true_type_infer_function, "type A = T extends { getState: () => infer /*@ Scope */ T } ? (/*START Scope*/number)/*END Scope*/: boolean;", + ok_conditional_true_type_infer_nested, "type A = MyType extends (OtherType extends infer /*@ InnerScope */T ? (/*START InnerScope*/infer /*@ OuterScope */ U)/*END InnerScope*/ : InnerFalse) ? (/*START OuterScope*/OuterTrue)/*END OuterScope*/ : OuterFalse;", +} diff --git a/crates/biome_js_semantic/src/tests/mod.rs b/crates/biome_js_semantic/src/tests/mod.rs index 9410a4f4c91b..e1eab4aada42 100644 --- a/crates/biome_js_semantic/src/tests/mod.rs +++ b/crates/biome_js_semantic/src/tests/mod.rs @@ -1,6 +1,7 @@ mod assertions; pub mod declarations; mod functions; +mod infer; mod references; mod scopes; diff --git a/crates/biome_js_semantic/src/tests/scopes.rs b/crates/biome_js_semantic/src/tests/scopes.rs index 4467b2821898..a747657ddf99 100644 --- a/crates/biome_js_semantic/src/tests/scopes.rs +++ b/crates/biome_js_semantic/src/tests/scopes.rs @@ -43,8 +43,8 @@ assert_semantics! { ok_scope_static_initialization_block, "class A { static/*START A*/ { - const a/*@ A*/ = 2; - }/*END A*/ + const a/*@ A*/ = 2; + }/*END A*/ };", } diff --git a/crates/biome_js_syntax/src/type_ext.rs b/crates/biome_js_syntax/src/type_ext.rs index 889e651b4ae3..6386d226d4b3 100644 --- a/crates/biome_js_syntax/src/type_ext.rs +++ b/crates/biome_js_syntax/src/type_ext.rs @@ -1,6 +1,7 @@ +use biome_rowan::AstNode; use std::iter; -use crate::AnyTsType; +use crate::{AnyTsType, TsConditionalType, TsInferType, TsTypeParameterName}; impl AnyTsType { /// Try to extract non `TsParenthesizedType` from `AnyTsType` @@ -77,4 +78,58 @@ impl AnyTsType { | AnyTsType::TsStringType(_) ) } + + /// Checks if `self` stands as the `true_type` of a conditional type in Typescript. + /// + /// # Examples + /// + /// ```rust + /// use biome_js_factory::make; + /// use biome_js_syntax::T; + /// use biome_js_syntax::AnyTsType; + /// + /// let check_type = AnyTsType::TsNumberType(make::ts_number_type(make::token(T![number]))); + /// let extends_type = AnyTsType::TsNumberType(make::ts_number_type(make::token(T![number]))); + /// let true_type = AnyTsType::TsNumberType(make::ts_number_type(make::token(T![number]))); + /// let false_type = AnyTsType::TsNumberType(make::ts_number_type(make::token(T![number]))); + /// + /// let conditional = make::ts_conditional_type( + /// check_type, + /// make::token(T![extends]), + /// extends_type, + /// make::token(T![?]), + /// true_type, + /// make::token(T![:]), + /// false_type, + /// ); + /// + /// assert!(!conditional.check_type().unwrap().in_conditional_true_type()); + /// assert!(!conditional.extends_type().unwrap().in_conditional_true_type()); + /// assert!(conditional.true_type().unwrap().in_conditional_true_type()); + /// assert!(!conditional.false_type().unwrap().in_conditional_true_type()); + /// ``` + pub fn in_conditional_true_type(&self) -> bool { + self.parent::() + .and_then(|parent| parent.true_type().ok()) + .map_or(false, |ref true_type| true_type == self) + } +} + +impl TsTypeParameterName { + /// Checks if `self` is the type being inferred in a TypeScript `TsInferType`. + /// + /// # Examples + /// + /// ```rust + /// use biome_js_factory::make; + /// use biome_js_syntax::T; + /// + /// let infer = make::ts_infer_type(make::token(T![infer]), make::ts_type_parameter_name(make::ident("T"))).build(); + /// assert!(infer.name().unwrap().in_infer_type()); + /// ``` + pub fn in_infer_type(&self) -> bool { + self.parent::() + .and_then(|parent| parent.name().ok()) + .map_or(false, |ref name| name == self) + } } diff --git a/website/src/content/docs/internals/changelog.mdx b/website/src/content/docs/internals/changelog.mdx index 363f0e7f683b..1c1195d6a101 100644 --- a/website/src/content/docs/internals/changelog.mdx +++ b/website/src/content/docs/internals/changelog.mdx @@ -38,6 +38,10 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom ### JavaScript APIs ### Linter +#### Bug fixes + +- Fix [#243](https://github.com/biomejs/biome/issues/243) a false positive case where the incorrect scope was defined for the `infer` type. in rule [noUndeclaredVariables](https://biomejs.dev/linter/rules/no-undeclared-variables/). Contributed by @denbezrukov + #### New features - Add [noMisleadingInstantiator](https://biomejs.dev/linter/rules/no-mileading-instantiator) rule. The rule reports the misleading use of the `new` and `constructor` methods. Contributed by @unvalley