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
}