oxc/crates/oxc_semantic/tests/util/mod.rs
Boshen 2f48bdf26f
fix(parser,semantic): make semantic own Trivias (#711)
closes #708

Making the parser return Rc<Trivias> is not a good API, and ideally
`Semantic` should just own `Trivias` so it can process or mutate it.
2023-08-10 15:30:32 +08:00

295 lines
10 KiB
Rust

use std::sync::Arc;
use itertools::Itertools;
use oxc_allocator::Allocator;
use oxc_diagnostics::{
miette::{miette, NamedSource},
Error,
};
extern crate miette;
use oxc_semantic::{Reference, Semantic, SemanticBuilder, SymbolFlags, SymbolId};
use oxc_span::{Atom, SourceType};
pub struct SemanticTester {
allocator: Allocator,
source_type: SourceType,
source_text: &'static str,
/// SemanticBuilder option
use_module_record_builder: bool,
}
impl SemanticTester {
/// Create a new tester for a TypeScript test case.
///
/// Use [`SemanticTester::js`] for JavaScript test cases.
#[allow(dead_code)]
pub fn ts(source_text: &'static str) -> Self {
Self::new(source_text, SourceType::default().with_module(true).with_typescript(true))
}
/// Create a new tester for a JavaScript test case.
///
/// Use [`SemanticTester::ts`] for TypeScript test cases.
pub fn js(source_text: &'static str) -> Self {
Self::new(source_text, SourceType::default().with_module(true))
}
pub fn new(source_text: &'static str, source_type: SourceType) -> Self {
Self {
allocator: Allocator::default(),
source_type,
source_text,
use_module_record_builder: true,
}
}
/// Set the [`SourceType`] to TypeScript (or JavaScript, using `false`)
#[allow(dead_code)]
pub fn with_typescript(mut self, yes: bool) -> Self {
self.source_type = SourceType::default().with_typescript(yes);
self
}
/// Mark the [`SourceType`] as JSX
#[allow(dead_code)]
pub fn with_jsx(mut self, yes: bool) -> Self {
self.source_type = self.source_type.with_jsx(yes);
self
}
/// Set [`SemanticBuilder`]'s `with_module_record_builder` option
#[allow(dead_code)]
pub fn with_module_record_builder(mut self, yes: bool) -> Self {
self.use_module_record_builder = yes;
self
}
/// Parse the source text and produce a new [`Semantic`]
#[allow(unstable_name_collisions)]
pub fn build(&self) -> Semantic<'_> {
let parse =
oxc_parser::Parser::new(&self.allocator, self.source_text, self.source_type).parse();
assert!(
parse.errors.is_empty(),
"\n Failed to parse source:\n{}\n\n{}",
self.source_text,
parse
.errors
.iter()
.map(|e| format!("{e}"))
.intersperse("\n\n".to_owned())
.collect::<String>()
);
let program = self.allocator.alloc(parse.program);
let semantic_ret = SemanticBuilder::new(self.source_text, self.source_type)
.with_check_syntax_error(true)
.with_trivias(parse.trivias)
.with_module_record_builder(self.use_module_record_builder)
.build(program);
if !semantic_ret.errors.is_empty() {
let report = self.wrap_diagnostics(semantic_ret.errors);
panic!(
"Semantic analysis failed:\n\n{}",
report
.iter()
.map(ToString::to_string)
.intersperse("\n\n".to_owned())
.collect::<String>()
);
};
semantic_ret.semantic
}
/// Tests that a symbol with the given name exists at the top-level scope and provides a
/// wrapper for writing assertions about the found symbol.
///
/// ## Fails
/// If no symbol with the given name exists at the top-level scope.
#[allow(dead_code)]
pub fn has_root_symbol(&self, name: &str) -> SymbolTester {
SymbolTester::new_at_root(self, self.build(), name)
}
/// Finds some symbol by name in the source code.
///
/// ## Fails
/// 1. No symbol with the given name exists,
/// 2. More than one symbol with the given name exists, so a symbol cannot
/// be uniquely obtained.
pub fn has_some_symbol(&self, name: &str) -> SymbolTester {
SymbolTester::new_unique(self, self.build(), name)
}
fn wrap_diagnostics(&self, diagnostics: Vec<Error>) -> Vec<Error> {
let name = "test".to_owned()
+ match (self.source_type.is_javascript(), self.source_type.is_jsx()) {
(true, true) => ".jsx",
(true, false) => ".js",
(false, true) => ".tsx",
(false, false) => ".ts",
};
let source = Arc::new(NamedSource::new(name, self.source_text.to_owned()));
diagnostics
.into_iter()
.map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source)))
.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<SymbolId, oxc_diagnostics::Error>,
}
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<F>(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<SymbolTester<'a>> for Result<(), Error> {
fn from(val: SymbolTester<'a>) -> Self {
val.test_result.map(|_| {}).map_err(|e| e.with_source_code(val.parent.source_text))
}
}