diff --git a/crates/rome_js_analyze/src/utils/tests.rs b/crates/rome_js_analyze/src/utils/tests.rs index 981a4deb386..ec8438c1497 100644 --- a/crates/rome_js_analyze/src/utils/tests.rs +++ b/crates/rome_js_analyze/src/utils/tests.rs @@ -1,7 +1,7 @@ use super::rename::*; use crate::utils::batch::JsBatchMutation; use rome_diagnostics::file::FileId; -use rome_js_semantic::semantic_model; +use rome_js_semantic::{semantic_model, SemanticModelOptions}; use rome_js_syntax::{JsFormalParameter, JsIdentifierBinding, JsVariableDeclarator, SourceType}; use rome_rowan::{AstNode, BatchMutationExt, SyntaxNodeCast}; @@ -9,7 +9,7 @@ use rome_rowan::{AstNode, BatchMutationExt, SyntaxNodeCast}; /// Asserts the renaming worked. pub fn assert_rename_ok(before: &str, expected: &str) { let r = rome_js_parser::parse(before, FileId::zero(), SourceType::js_module()); - let model = semantic_model(&r.tree()); + let model = semantic_model(&r.tree(), SemanticModelOptions::default()); let binding_a = r .syntax() @@ -30,7 +30,7 @@ pub fn assert_rename_ok(before: &str, expected: &str) { /// Asserts the renaming to fail. pub fn assert_rename_nok(before: &str) { let r = rome_js_parser::parse(before, FileId::zero(), SourceType::js_module()); - let model = semantic_model(&r.tree()); + let model = semantic_model(&r.tree(), SemanticModelOptions::default()); let binding_a = r .syntax() diff --git a/crates/rome_js_semantic/src/semantic_model.rs b/crates/rome_js_semantic/src/semantic_model.rs index a39710667fb..d4a759294e1 100644 --- a/crates/rome_js_semantic/src/semantic_model.rs +++ b/crates/rome_js_semantic/src/semantic_model.rs @@ -116,10 +116,12 @@ struct SemanticModelData { declaration_all_reads: HashMap>, // Maps a declaration range to the range of its "writes" declaration_all_writes: HashMap>, - /// All references that could not be resolved - unresolved_references: Vec<(ReferenceType, TextRange)>, // All bindings that were exported exported: HashSet, + /// All references that could not be resolved + unresolved_references: Vec<(ReferenceType, TextRange)>, + /// All references that are resolved to globals + global_references: Vec<(ReferenceType, TextRange)>, } impl SemanticModelData { @@ -475,6 +477,34 @@ impl<'a> ExactSizeIterator for UnresolvedReferencesIter<'a> { impl<'a> FusedIterator for UnresolvedReferencesIter<'a> {} +pub struct GlobalsReferencesIter<'a> { + data: Arc, + iter: std::slice::Iter<'a, (ReferenceType, TextRange)>, +} + +impl<'a> Iterator for GlobalsReferencesIter<'a> { + type Item = Reference; + + fn next(&mut self) -> Option { + let (ty, range) = self.iter.next()?; + let node = self.data.node_by_range.get(range)?; + Some(Reference { + data: self.data.clone(), + node: node.clone(), + range: *range, + ty: *ty, + }) + } +} + +impl<'a> ExactSizeIterator for GlobalsReferencesIter<'a> { + fn len(&self) -> usize { + self.iter.len() + } +} + +impl<'a> FusedIterator for GlobalsReferencesIter<'a> {} + /// Iterate all bindings that were bound in a given scope. It **does /// not** Returns bindings of parent scopes. pub struct ScopeBindingsIter { @@ -556,11 +586,11 @@ impl SemanticModel { /// ```rust /// use rome_rowan::{AstNode, SyntaxNodeCast}; /// use rome_js_syntax::{SourceType, JsReferenceIdentifier}; - /// use rome_js_semantic::{semantic_model, SemanticScopeExtensions}; + /// use rome_js_semantic::{semantic_model, SemanticModelOptions, SemanticScopeExtensions}; /// use rome_diagnostics::file::FileId; /// /// let r = rome_js_parser::parse("function f(){let a = arguments[0]; let b = a + 1;}", FileId::zero(), SourceType::js_module()); - /// let model = semantic_model(&r.tree()); + /// let model = semantic_model(&r.tree(), SemanticModelOptions::default()); /// /// let arguments_reference = r /// .syntax() @@ -599,11 +629,11 @@ impl SemanticModel { /// ```rust /// use rome_rowan::{AstNode, SyntaxNodeCast}; /// use rome_js_syntax::{SourceType, JsReferenceIdentifier}; - /// use rome_js_semantic::{semantic_model, DeclarationExtensions}; + /// use rome_js_semantic::{semantic_model, DeclarationExtensions, SemanticModelOptions}; /// use rome_diagnostics::file::FileId; /// /// let r = rome_js_parser::parse("function f(){let a = arguments[0]; let b = a + 1;}", FileId::zero(), SourceType::js_module()); - /// let model = semantic_model(&r.tree()); + /// let model = semantic_model(&r.tree(), SemanticModelOptions::default()); /// /// let arguments_reference = r /// .syntax() @@ -633,11 +663,11 @@ impl SemanticModel { /// ```rust /// use rome_rowan::{AstNode, SyntaxNodeCast}; /// use rome_js_syntax::{SourceType, JsIdentifierBinding}; - /// use rome_js_semantic::{semantic_model, AllReferencesExtensions}; + /// use rome_js_semantic::{semantic_model, AllReferencesExtensions, SemanticModelOptions}; /// use rome_diagnostics::file::FileId; /// /// let r = rome_js_parser::parse("function f(){let a = arguments[0]; let b = a + 1;}", FileId::zero(), SourceType::js_module()); - /// let model = semantic_model(&r.tree()); + /// let model = semantic_model(&r.tree(), SemanticModelOptions::default()); /// /// let a_binding = r /// .syntax() @@ -684,6 +714,14 @@ impl SemanticModel { } } + /// Returns an iterator of all the globals references in the program + pub fn all_globals(&self) -> GlobalsReferencesIter<'_> { + GlobalsReferencesIter { + data: self.data.clone(), + iter: self.data.global_references.iter(), + } + } + /// Returns an iterator of all the unresolved references in the program pub fn all_unresolved_references(&self) -> UnresolvedReferencesIter<'_> { UnresolvedReferencesIter { @@ -796,32 +834,36 @@ impl ClosureExtensions for T {} /// and stored inside the [SemanticModel]. pub struct SemanticModelBuilder { root: JsAnyRoot, + node_by_range: HashMap, + globals: HashSet, scopes: Vec, scope_range_by_start: HashMap>>, scope_hoisted_to_by_range: HashMap, - node_by_range: HashMap, declarations_by_range: HashMap, declaration_all_references: HashMap>, declaration_all_reads: HashMap>, declaration_all_writes: HashMap>, - unresolved_references: Vec<(ReferenceType, TextRange)>, exported: HashSet, + unresolved_references: Vec<(ReferenceType, TextRange)>, + global_references: Vec<(ReferenceType, TextRange)>, } impl SemanticModelBuilder { pub fn new(root: JsAnyRoot) -> Self { Self { root, + node_by_range: HashMap::new(), + globals: HashSet::new(), scopes: vec![], scope_range_by_start: HashMap::new(), scope_hoisted_to_by_range: HashMap::new(), - node_by_range: HashMap::new(), declarations_by_range: HashMap::new(), declaration_all_references: HashMap::new(), declaration_all_reads: HashMap::new(), declaration_all_writes: HashMap::new(), - unresolved_references: Vec::new(), exported: HashSet::new(), + unresolved_references: Vec::new(), + global_references: Vec::new(), } } @@ -830,6 +872,11 @@ impl SemanticModelBuilder { self.node_by_range.insert(node.text_range(), node.clone()); } + #[inline] + pub fn push_global(&mut self, name: impl Into) { + self.globals.insert(name.into()); + } + #[inline] pub fn push_event(&mut self, e: SemanticEvent) { use SemanticEvent::*; @@ -970,7 +1017,14 @@ impl SemanticModelBuilder { ReferenceType::Write { hoisted: false } }; - self.unresolved_references.push((ty, range)); + let node = &self.node_by_range[&range]; + let name = node.text_trimmed().to_string(); + + if self.globals.get(&name).is_some() { + self.global_references.push((ty, range)); + } else { + self.unresolved_references.push((ty, range)); + } } Exported { range } => { self.exported.insert(range); @@ -996,19 +1050,33 @@ impl SemanticModelBuilder { declaration_all_references: self.declaration_all_references, declaration_all_reads: self.declaration_all_reads, declaration_all_writes: self.declaration_all_writes, - unresolved_references: self.unresolved_references, exported: self.exported, + unresolved_references: self.unresolved_references, + global_references: self.global_references, }; SemanticModel::new(data) } } +#[derive(Default)] +/// Extra options for the [SemanticModel] creation. +pub struct SemanticModelOptions { + /// All the allowed globals names + pub globals: HashSet, +} + /// Build the complete [SemanticModel] of a parsed file. /// For a push based model to build the [SemanticModel], see [SemanticModelBuilder]. -pub fn semantic_model(root: &JsAnyRoot) -> SemanticModel { +pub fn semantic_model(root: &JsAnyRoot, options: SemanticModelOptions) -> SemanticModel { let mut extractor = SemanticEventExtractor::default(); let mut builder = SemanticModelBuilder::new(root.clone()); + let SemanticModelOptions { globals } = options; + + for global in globals { + builder.push_global(global); + } + let root = root.syntax(); for node in root.preorder() { match node { @@ -1041,7 +1109,7 @@ mod test { FileId::zero(), SourceType::js_module(), ); - let model = semantic_model(&r.tree()); + let model = semantic_model(&r.tree(), SemanticModelOptions::default()); let arguments_reference = r .syntax() @@ -1145,7 +1213,7 @@ mod test { FileId::zero(), SourceType::js_module(), ); - let model = semantic_model(&r.tree()); + let model = semantic_model(&r.tree(), SemanticModelOptions::default()); let function_f = r .syntax() @@ -1183,7 +1251,7 @@ mod test { /// Finds the last time a token named "name" is used and see if its node is marked as exported fn assert_is_exported(is_exported: bool, name: &str, code: &str) { let r = rome_js_parser::parse(code, FileId::zero(), SourceType::tsx()); - let model = semantic_model(&r.tree()); + let model = semantic_model(&r.tree(), SemanticModelOptions::default()); let node = r .syntax() @@ -1312,4 +1380,21 @@ mod test { assert_is_exported(true, "A", "enum A {}; exports = A"); assert_is_exported(true, "A", "enum A {}; exports.A = A"); } + + #[test] + pub fn ok_semantic_model_globals() { + let r = rome_js_parser::parse("console.log()", FileId::zero(), SourceType::js_module()); + + let mut options = SemanticModelOptions::default(); + options.globals.insert("console".into()); + + let model = semantic_model(&r.tree(), options); + + let globals: Vec<_> = model.all_globals().collect(); + + assert_eq!(globals.len(), 1); + assert!(globals[0].declaration().is_none()); + assert!(globals[0].is_read()); + assert_eq!(globals[0].node().text_trimmed(), "console"); + } } diff --git a/crates/rome_js_semantic/src/semantic_model/closure.rs b/crates/rome_js_semantic/src/semantic_model/closure.rs index 37c57933803..1b449d875aa 100644 --- a/crates/rome_js_semantic/src/semantic_model/closure.rs +++ b/crates/rome_js_semantic/src/semantic_model/closure.rs @@ -285,7 +285,7 @@ mod test { fn assert_closure(code: &str, name: &str, captures: &[&str]) { let r = rome_js_parser::parse(code, FileId::zero(), SourceType::tsx()); - let model = semantic_model(&r.tree()); + let model = semantic_model(&r.tree(), SemanticModelOptions::default()); let closure = if name != "ARROWFUNCTION" { let node = r @@ -327,7 +327,7 @@ mod test { fn get_closure_children(code: &str, name: &str) -> Vec { let r = rome_js_parser::parse(code, FileId::zero(), SourceType::tsx()); - let model = semantic_model(&r.tree()); + let model = semantic_model(&r.tree(), SemanticModelOptions::default()); let closure = if name != "ARROWFUNCTION" { let node = r diff --git a/crates/rome_service/src/file_handlers/javascript.rs b/crates/rome_service/src/file_handlers/javascript.rs index 733a168571c..82b8582dda6 100644 --- a/crates/rome_service/src/file_handlers/javascript.rs +++ b/crates/rome_service/src/file_handlers/javascript.rs @@ -8,7 +8,7 @@ use rome_js_analyze::{analyze, analyze_with_inspect_matcher, metadata, RuleError use rome_js_formatter::context::{QuoteProperties, QuoteStyle}; use rome_js_formatter::{context::JsFormatOptions, format_node}; use rome_js_parser::Parse; -use rome_js_semantic::semantic_model; +use rome_js_semantic::{semantic_model, SemanticModelOptions}; use rome_js_syntax::{ JsAnyRoot, JsLanguage, JsSyntaxNode, SourceType, TextRange, TextSize, TokenAtOffset, }; @@ -456,7 +456,7 @@ fn rename( new_name: String, ) -> Result { let root = parse.tree(); - let model = semantic_model(&root); + let model = semantic_model(&root, SemanticModelOptions::default()); if let Some(node) = parse .syntax()