diff --git a/crates/oxc_semantic/tests/scopes.rs b/crates/oxc_semantic/tests/scopes.rs new file mode 100644 index 000000000..9c7c84aa6 --- /dev/null +++ b/crates/oxc_semantic/tests/scopes.rs @@ -0,0 +1,92 @@ +mod util; + +use oxc_semantic::ScopeFlags; +use util::{Expect, SemanticTester}; + +#[test] +fn test_top_level_strict() { + // Module with top-level "use strict" + SemanticTester::js( + r#" + "use strict"; + function foo() { + return 1 + } + "#, + ) + .has_root_symbol("foo") + .is_in_scope(ScopeFlags::Top | ScopeFlags::StrictMode) + // .expect(expect_strict) + .test(); + + // Module without top-level "use strict" + SemanticTester::js( + r#" + function foo() { + return 1 + } + "#, + ) + .has_root_symbol("foo") + .is_in_scope(ScopeFlags::Top | ScopeFlags::StrictMode) + .test(); + + // Script with top-level "use strict" + SemanticTester::js( + r#" + "use strict"; + function foo() { + return 1 + } + "#, + ) + .with_module(false) + .has_root_symbol("foo") + .is_in_scope(ScopeFlags::Top | ScopeFlags::StrictMode) + .test(); + + // Script without top-level "use strict" + SemanticTester::js( + r#" + function foo() { + return 1 + } + "#, + ) + .with_module(false) + .has_root_symbol("foo") + .is_in_scope(ScopeFlags::Top) + .is_not_in_scope(ScopeFlags::StrictMode) + .test(); +} + +#[test] +fn test_function_level_strict() { + let tester = SemanticTester::js( + r#" + function foo() { + "use strict"; + let x = 1; + return x + } + "#, + ) + .with_module(false); + + tester.has_some_symbol("x") + .is_in_scope(ScopeFlags::StrictMode | ScopeFlags::Function) + .expect(|(semantic, symbol_id)| -> Result<(), &'static str> { + let scope_id = semantic.symbol_scope(symbol_id); + let Some(parent_scope_id) = semantic.scopes().get_parent_id(scope_id) else { + return Err("Expected x's scope to have a parent") + }; + let parent_flags = semantic.scopes().get_flags(parent_scope_id); + if parent_flags.contains(ScopeFlags::Top) { + Ok(()) + } else { + Err("Expected x to be in a top-level function declaration, but its parent scope has flags {parent_flags:?}") + } + }) + .test(); + tester.has_some_symbol("foo").is_not_in_scope(ScopeFlags::StrictMode).test(); +} diff --git a/crates/oxc_semantic/tests/symbols.rs b/crates/oxc_semantic/tests/symbols.rs index b80a8b08a..65f0b7774 100644 --- a/crates/oxc_semantic/tests/symbols.rs +++ b/crates/oxc_semantic/tests/symbols.rs @@ -1,4 +1,5 @@ mod util; + use oxc_semantic::SymbolFlags; use util::SemanticTester; diff --git a/crates/oxc_semantic/tests/util/expect.rs b/crates/oxc_semantic/tests/util/expect.rs new file mode 100644 index 000000000..de49affb6 --- /dev/null +++ b/crates/oxc_semantic/tests/util/expect.rs @@ -0,0 +1,5 @@ +pub trait Expect { + fn expect(self, expectation: F) -> Self + where + F: FnOnce(P) -> R; +} diff --git a/crates/oxc_semantic/tests/util/mod.rs b/crates/oxc_semantic/tests/util/mod.rs index 4b087a281..721251be6 100644 --- a/crates/oxc_semantic/tests/util/mod.rs +++ b/crates/oxc_semantic/tests/util/mod.rs @@ -1,14 +1,16 @@ +mod expect; +mod symbol_tester; use std::{path::PathBuf, sync::Arc}; use itertools::Itertools; use oxc_allocator::Allocator; -use oxc_diagnostics::{ - miette::{miette, NamedSource}, - Error, -}; +use oxc_diagnostics::{miette::NamedSource, Error}; extern crate miette; -use oxc_semantic::{Reference, Semantic, SemanticBuilder, SymbolFlags, SymbolId}; -use oxc_span::{Atom, SourceType}; +use oxc_semantic::{Semantic, SemanticBuilder}; +use oxc_span::SourceType; + +pub use expect::Expect; +pub use symbol_tester::SymbolTester; pub struct SemanticTester { allocator: Allocator, @@ -50,6 +52,12 @@ impl SemanticTester { self } + #[allow(dead_code)] + pub fn with_module(mut self, yes: bool) -> Self { + self.source_type = self.source_type.with_module(yes); + self + } + /// Parse the source text and produce a new [`Semantic`] #[allow(unstable_name_collisions)] pub fn build(&self) -> Semantic<'_> { @@ -126,156 +134,3 @@ impl SemanticTester { .collect() } } - -pub struct SymbolTester<'a> { - parent: &'a SemanticTester, - /// Reference to semantic analysis results, from [`SemanticTester`] - semantic: Semantic<'a>, - /// Name of the subject symbol - target_symbol_name: String, - /// Symbol data, or error if not found - test_result: Result, -} - -impl<'a> SymbolTester<'a> { - #[allow(dead_code)] - pub(super) fn new_at_root( - parent: &'a SemanticTester, - semantic: Semantic<'a>, - target: &str, - ) -> Self { - let decl = - semantic.scopes().get_binding(semantic.scopes().root_scope_id(), &Atom::from(target)); - let data = decl.map_or_else(|| Err(miette!("Could not find declaration for {target}")), Ok); - - SymbolTester { parent, semantic, target_symbol_name: target.to_string(), test_result: data } - } - - pub(super) fn new_unique( - parent: &'a SemanticTester, - semantic: Semantic<'a>, - target: &str, - ) -> Self { - let symbols_with_target_name: Vec<_> = - semantic.scopes().iter_bindings().filter(|(_, _, name)| name == &target).collect(); - let data = match symbols_with_target_name.len() { - 0 => Err(miette!("Could not find declaration for {target}")), - 1 => Ok(symbols_with_target_name.iter().map(|(_, symbol_id, _)| *symbol_id).next().unwrap()), - n if n > 1 => Err(miette!("Couldn't uniquely resolve symbol id for target {target}; {n} symbols with that name are declared in the source.")), - _ => unreachable!() - }; - - SymbolTester { parent, semantic, target_symbol_name: target.to_string(), test_result: data } - } - - /// Checks if the resolved symbol contains all flags in `flags`, using [`SymbolFlags::contains()`] - pub fn contains_flags(mut self, flags: SymbolFlags) -> Self { - self.test_result = match self.test_result { - Ok(symbol_id) => { - let found_flags = self.semantic.symbols().get_flag(symbol_id); - if found_flags.contains(flags) { - Ok(symbol_id) - } else { - Err(miette!( - "Expected {} to contain flags {:?}, but it had {:?}", - self.target_symbol_name, - flags, - found_flags - )) - } - } - err => err, - }; - self - } - - pub fn intersects_flags(mut self, flags: SymbolFlags) -> Self { - self.test_result = match self.test_result { - Ok(symbol_id) => { - let found_flags = self.semantic.symbols().get_flag(symbol_id); - if found_flags.intersects(flags) { - Ok(symbol_id) - } else { - Err(miette!( - "Expected {} to intersect with flags {:?}, but it had {:?}", - self.target_symbol_name, - flags, - found_flags - )) - } - } - err => err, - }; - self - } - - pub fn has_number_of_reads(self, ref_count: usize) -> Self { - self.has_number_of_references_where(ref_count, Reference::is_read) - } - - #[allow(dead_code)] - pub fn has_number_of_writes(self, ref_count: usize) -> Self { - self.has_number_of_references_where(ref_count, Reference::is_write) - } - - pub fn has_number_of_references(self, ref_count: usize) -> Self { - self.has_number_of_references_where(ref_count, |_| true) - } - - pub fn has_number_of_references_where(mut self, ref_count: usize, filter: F) -> Self - where - F: FnMut(&Reference) -> bool, - { - self.test_result = match self.test_result { - Ok(symbol_id) => { - let refs = { - self.semantic - .symbols() - .get_resolved_reference_ids(symbol_id) - .iter() - .map(|r_id| self.semantic.symbols().get_reference(*r_id).clone()) - }; - let num_accepted = refs.filter(filter).count(); - if num_accepted == ref_count { - Ok(symbol_id) - } else { - Err(miette!("Expected to find {ref_count} acceptable references, but only found {num_accepted}")) - } - } - e => e, - }; - self - } - - #[allow(clippy::wrong_self_convention)] - pub fn is_exported(mut self) -> Self { - self.test_result = match self.test_result { - Ok(symbol_id) => { - let binding = Atom::from(self.target_symbol_name.clone()); - if self.semantic.module_record().exported_bindings.contains_key(&binding) - && self.semantic.scopes().get_root_binding(&binding) == Some(symbol_id) - { - Ok(symbol_id) - } else { - Err(miette!("Expected {binding} to be exported.")) - } - } - e => e, - }; - self - } - - /// Complete the test case. Will panic if any of the previously applied - /// assertions failed. - pub fn test(self) { - let res: Result<_, _> = self.into(); - - res.unwrap(); - } -} - -impl<'a> From> for Result<(), Error> { - fn from(val: SymbolTester<'a>) -> Self { - val.test_result.map(|_| {}).map_err(|e| e.with_source_code(val.parent.source_text)) - } -} diff --git a/crates/oxc_semantic/tests/util/symbol_tester.rs b/crates/oxc_semantic/tests/util/symbol_tester.rs new file mode 100644 index 000000000..4179593c2 --- /dev/null +++ b/crates/oxc_semantic/tests/util/symbol_tester.rs @@ -0,0 +1,251 @@ +use std::rc::Rc; + +use oxc_diagnostics::{miette::miette, Error}; +extern crate miette; +use oxc_semantic::{Reference, ScopeFlags, Semantic, SymbolFlags, SymbolId}; +use oxc_span::Atom; + +use super::{Expect, SemanticTester}; + +pub struct SymbolTester<'a> { + parent: &'a SemanticTester, + /// Reference to semantic analysis results, from [`SemanticTester`] + semantic: Rc>, + /// Name of the subject symbol + target_symbol_name: String, + /// Symbol data, or error if not found + test_result: Result, +} + +impl<'a> SymbolTester<'a> { + #[allow(dead_code)] + pub(super) fn new_at_root( + parent: &'a SemanticTester, + semantic: Semantic<'a>, + target: &str, + ) -> Self { + let decl = + semantic.scopes().get_binding(semantic.scopes().root_scope_id(), &Atom::from(target)); + let data = decl.map_or_else(|| Err(miette!("Could not find declaration for {target}")), Ok); + + SymbolTester { + parent, + semantic: Rc::new(semantic), + target_symbol_name: target.to_string(), + test_result: data, + } + } + + pub(super) fn new_unique( + parent: &'a SemanticTester, + semantic: Semantic<'a>, + target: &str, + ) -> Self { + let symbols_with_target_name: Vec<_> = + semantic.scopes().iter_bindings().filter(|(_, _, name)| name == &target).collect(); + let data = match symbols_with_target_name.len() { + 0 => Err(miette!("Could not find declaration for {target}")), + 1 => Ok(symbols_with_target_name.iter().map(|(_, symbol_id, _)| *symbol_id).next().unwrap()), + n if n > 1 => Err(miette!("Couldn't uniquely resolve symbol id for target {target}; {n} symbols with that name are declared in the source.")), + _ => unreachable!() + }; + + SymbolTester { + parent, + semantic: Rc::new(semantic), + target_symbol_name: target.to_string(), + test_result: data, + } + } + + /// Checks if the resolved symbol contains all flags in `flags`, using [`SymbolFlags::contains()`] + pub fn contains_flags(mut self, flags: SymbolFlags) -> Self { + self.test_result = match self.test_result { + Ok(symbol_id) => { + let found_flags = self.semantic.symbols().get_flag(symbol_id); + if found_flags.contains(flags) { + Ok(symbol_id) + } else { + Err(miette!( + "Expected {} to contain flags {:?}, but it had {:?}", + self.target_symbol_name, + flags, + found_flags + )) + } + } + err => err, + }; + self + } + + pub fn intersects_flags(mut self, flags: SymbolFlags) -> Self { + self.test_result = match self.test_result { + Ok(symbol_id) => { + let found_flags = self.semantic.symbols().get_flag(symbol_id); + if found_flags.intersects(flags) { + Ok(symbol_id) + } else { + Err(miette!( + "Expected {} to intersect with flags {:?}, but it had {:?}", + self.target_symbol_name, + flags, + found_flags + )) + } + } + err => err, + }; + self + } + + pub fn has_number_of_reads(self, ref_count: usize) -> Self { + self.has_number_of_references_where(ref_count, Reference::is_read) + } + + #[allow(dead_code)] + pub fn has_number_of_writes(self, ref_count: usize) -> Self { + self.has_number_of_references_where(ref_count, Reference::is_write) + } + + pub fn has_number_of_references(self, ref_count: usize) -> Self { + self.has_number_of_references_where(ref_count, |_| true) + } + + pub fn has_number_of_references_where(mut self, ref_count: usize, filter: F) -> Self + where + F: FnMut(&Reference) -> bool, + { + self.test_result = match self.test_result { + Ok(symbol_id) => { + let refs = { + self.semantic + .symbols() + .get_resolved_reference_ids(symbol_id) + .iter() + .map(|r_id| self.semantic.symbols().get_reference(*r_id).clone()) + }; + let num_accepted = refs.filter(filter).count(); + if num_accepted == ref_count { + Ok(symbol_id) + } else { + Err(miette!("Expected to find {ref_count} acceptable references, but only found {num_accepted}")) + } + } + e => e, + }; + self + } + + #[allow(clippy::wrong_self_convention)] + pub fn is_exported(mut self) -> Self { + self.test_result = match self.test_result { + Ok(symbol_id) => { + let binding = Atom::from(self.target_symbol_name.clone()); + if self.semantic.module_record().exported_bindings.contains_key(&binding) + && self.semantic.scopes().get_root_binding(&binding) == Some(symbol_id) + { + Ok(symbol_id) + } else { + Err(miette!("Expected {binding} to be exported.")) + } + } + e => e, + }; + self + } + + #[allow(clippy::wrong_self_convention)] + pub fn is_in_scope(mut self, expected_flags: ScopeFlags) -> Self { + let target_name: &str = self.target_symbol_name.as_ref(); + self.test_result = match self.test_result { + Ok(symbol_id) => { + let scope_id = self.semantic.symbol_scope(symbol_id); + let scope_flags = self.semantic.scopes().get_flags(scope_id); + if scope_flags.contains(expected_flags) { + Ok(symbol_id) + } else { + Err(miette!("Binding {target_name} is not in a scope with expected flags.\n\tExpected: {expected_flags:?}\n\tActual: {scope_flags:?}")) + } + } + e => e, + }; + self + } + + #[allow(clippy::wrong_self_convention)] + pub fn is_not_in_scope(mut self, excluded_flags: ScopeFlags) -> Self { + let target_name: &str = self.target_symbol_name.as_ref(); + self.test_result = match self.test_result { + Ok(symbol_id) => { + let scope_id = self.semantic.symbol_scope(symbol_id); + let scope_flags = self.semantic.scopes().get_flags(scope_id); + if scope_flags.contains(excluded_flags) { + Err(miette!("Binding {target_name} is in a scope with excluded flags.\n\tExpected: not {excluded_flags:?}\n\tActual: {scope_flags:?}")) + } else { + Ok(symbol_id) + } + } + e => e, + }; + self + } + + /// Complete the test case. Will panic if any of the previously applied + /// assertions failed. + pub fn test(self) { + let res: Result<_, _> = self.into(); + + res.unwrap(); + } +} + +impl<'a> Expect<(Rc>, SymbolId), bool> for SymbolTester<'a> { + fn expect<'e, F>(self, expectation: F) -> Self + where + F: FnOnce((Rc>, SymbolId)) -> bool, + { + let Ok(symbol_id) = self.test_result else { return self }; + let did_pass = expectation((Rc::clone(&self.semantic), symbol_id)); + if did_pass { + self + } else { + Self { test_result: Err(miette!("Expectation failed")), ..self } + } + } +} + +impl<'a> Expect<(Rc>, SymbolId), Result<(), &'static str>> for SymbolTester<'a> { + fn expect<'e, F>(self, expectation: F) -> Self + where + F: FnOnce((Rc>, SymbolId)) -> Result<(), &'static str>, + { + let Ok(symbol_id) = self.test_result else { return self }; + let did_pass = expectation((Rc::clone(&self.semantic), symbol_id)); + if let Err(e) = did_pass { + Self { test_result: Err(miette!(e)), ..self } + } else { + self + } + } +} +impl<'a> Expect<(Rc>, SymbolId), Result<(), Error>> for SymbolTester<'a> { + fn expect<'e, F>(self, expectation: F) -> Self + where + F: FnOnce((Rc>, SymbolId)) -> Result<(), Error>, + { + let Ok(symbol_id) = self.test_result else { return self }; + let did_pass = expectation((Rc::clone(&self.semantic), symbol_id)); + if let Err(e) = did_pass { + Self { test_result: Err(e), ..self } + } else { + self + } + } +} + +impl<'a> From> for Result<(), Error> { + fn from(val: SymbolTester<'a>) -> Self { + val.test_result.map(|_| {}).map_err(|e| e.with_source_code(val.parent.source_text)) + } +} diff --git a/crates/oxc_span/src/source_type.rs b/crates/oxc_span/src/source_type.rs index 48f3761d6..cd2bb85ff 100644 --- a/crates/oxc_span/src/source_type.rs +++ b/crates/oxc_span/src/source_type.rs @@ -110,6 +110,8 @@ impl SourceType { pub fn with_module(mut self, yes: bool) -> Self { if yes { self.module_kind = ModuleKind::Module; + } else { + self.module_kind = ModuleKind::Script; } self }