mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
275 lines
9.3 KiB
Rust
275 lines
9.3 KiB
Rust
mod class_tester;
|
|
mod expect;
|
|
mod symbol_tester;
|
|
use std::sync::Arc;
|
|
|
|
pub use class_tester::ClassTester;
|
|
pub use expect::Expect;
|
|
use itertools::Itertools;
|
|
use oxc_allocator::Allocator;
|
|
use oxc_cfg::DisplayDot;
|
|
use oxc_diagnostics::{Error, NamedSource, OxcDiagnostic};
|
|
use oxc_semantic::{dot::DebugDot, Semantic, SemanticBuilder, SemanticBuilderReturn};
|
|
use oxc_span::SourceType;
|
|
pub use symbol_tester::SymbolTester;
|
|
|
|
#[must_use]
|
|
pub struct SemanticTester<'a> {
|
|
/// Memory arena that AST and [`Semantic`] will store data in.
|
|
allocator: Allocator,
|
|
/// Source type of the test case.
|
|
source_type: SourceType,
|
|
/// The source text of the test case.
|
|
source_text: &'a str,
|
|
/// Build a [`ControlFlowGraph`]?
|
|
///
|
|
/// [`ControlFlowGraph`]: oxc_cfg::ControlFlowGraph
|
|
cfg: bool,
|
|
/// Expect semantic analysis to produce errors.
|
|
///
|
|
/// Default is `false`.
|
|
expect_errors: bool,
|
|
}
|
|
|
|
impl<'a> SemanticTester<'a> {
|
|
/// Create a new tester for a TypeScript test case.
|
|
///
|
|
/// Use [`SemanticTester::js`] for JavaScript test cases.
|
|
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 TypeScript test case with JSX.
|
|
///
|
|
/// Use [`SemanticTester::ts`] for TypeScript test cases without JSX.
|
|
pub fn tsx(source_text: &'static str) -> Self {
|
|
Self::ts(source_text).with_jsx(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))
|
|
}
|
|
|
|
/// Create a new tester for some source text.
|
|
///
|
|
/// You may find one of the other factory methods, like
|
|
/// [`SemanticTester::ts`] or [`SemanticTester::js`], more convenient.
|
|
pub fn new(source_text: &'a str, source_type: SourceType) -> Self {
|
|
Self {
|
|
allocator: Allocator::default(),
|
|
source_type,
|
|
source_text,
|
|
cfg: false,
|
|
expect_errors: false,
|
|
}
|
|
}
|
|
|
|
/// Set the [`SourceType`] to TypeScript (or JavaScript, using `false`)
|
|
pub fn with_typescript(mut self, yes: bool) -> Self {
|
|
self.source_type = SourceType::default().with_typescript(yes);
|
|
self
|
|
}
|
|
|
|
/// Mark the [`SourceType`] as JSX
|
|
///
|
|
/// If the source is currently TypeScript, it will become TSX.
|
|
pub fn with_jsx(mut self, yes: bool) -> Self {
|
|
self.source_type = self.source_type.with_jsx(yes);
|
|
self
|
|
}
|
|
|
|
/// Mark the [`SourceType`] as an ESM module.
|
|
pub fn with_module(mut self, yes: bool) -> Self {
|
|
self.source_type = self.source_type.with_module(yes);
|
|
self
|
|
}
|
|
|
|
/// Enable or disable building a [`ControlFlowGraph`].
|
|
///
|
|
/// [`ControlFlowGraph`]: oxc_cfg::ControlFlowGraph
|
|
pub fn with_cfg(mut self, yes: bool) -> Self {
|
|
self.cfg = yes;
|
|
self
|
|
}
|
|
|
|
/// The program being tested is expected to produce errors during semantic analysis.
|
|
///
|
|
/// By default, programs are expected to be error-free.
|
|
///
|
|
/// # Example
|
|
/// ```
|
|
/// use crate::util::{SemanticTester as T};
|
|
///
|
|
/// // Default behavior. You could omit `expect_errors(false)` here
|
|
/// T::js("function foo(a, a) { }").expect_errors(false).has_root_symbol("foo").test()
|
|
/// // Not allowed in TS
|
|
/// T::ts("function foo(a, a) { }").expect_errors(true).has_root_symbol("foo").test()
|
|
/// ```
|
|
pub fn expect_errors(mut self, yes: bool) -> Self {
|
|
self.expect_errors = yes;
|
|
self
|
|
}
|
|
|
|
/// Parse the source text and produce a new [`Semantic`].
|
|
///
|
|
/// Normally this will panic if semantic analysis produces any errors. Use
|
|
/// # Panics
|
|
/// - if parsing fails
|
|
/// - if semantic analysis does/does not produce errors as expected
|
|
#[allow(unstable_name_collisions)]
|
|
pub fn build(&self) -> Semantic<'_> {
|
|
let semantic_ret = self.build_with_errors();
|
|
match (self.expect_errors, semantic_ret.errors.is_empty()) {
|
|
(true, true) => panic!("Expected errors, but none were produced"),
|
|
(false, false) => panic!(
|
|
"Semantic analysis failed:\n\n{}",
|
|
semantic_ret
|
|
.errors
|
|
.iter()
|
|
.map(ToString::to_string)
|
|
.intersperse("\n\n".to_owned())
|
|
.collect::<String>()
|
|
),
|
|
_ => semantic_ret.semantic,
|
|
}
|
|
}
|
|
|
|
/// Parse the source text into a new [`Semantic`], but preserves any errors that occur during
|
|
/// semantic analysis
|
|
/// # Panics
|
|
///
|
|
#[allow(unstable_name_collisions)]
|
|
pub fn build_with_errors(&self) -> SemanticBuilderReturn<'_> {
|
|
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);
|
|
SemanticBuilder::new(self.source_text, self.source_type)
|
|
.with_check_syntax_error(true)
|
|
.with_trivias(parse.trivias)
|
|
.with_cfg(self.cfg)
|
|
.build(program)
|
|
}
|
|
|
|
pub fn basic_blocks_count(&self) -> usize {
|
|
let built = self.build();
|
|
built.cfg().map_or(0, |cfg| cfg.basic_blocks.len())
|
|
}
|
|
|
|
pub fn basic_blocks_printed(&self) -> String {
|
|
let built = self.build();
|
|
built.cfg().map_or_else(String::default, |cfg| {
|
|
cfg.basic_blocks
|
|
.iter()
|
|
.map(DisplayDot::display_dot)
|
|
.enumerate()
|
|
.map(|(i, it)| {
|
|
format!(
|
|
"bb{i}: {{\n{}\n}}",
|
|
it.lines().map(|x| format!("\t{}", x.trim())).join("\n")
|
|
)
|
|
})
|
|
.join("\n\n")
|
|
})
|
|
}
|
|
|
|
pub fn cfg_dot_diagram(&self) -> String {
|
|
let semantic = self.build();
|
|
semantic.cfg().map_or_else(String::default, |cfg| cfg.debug_dot(semantic.nodes().into()))
|
|
}
|
|
|
|
/// 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.
|
|
pub fn has_root_symbol(&self, name: &str) -> SymbolTester {
|
|
SymbolTester::new_at_root(self, self.build(), name)
|
|
}
|
|
|
|
/// Find first symbol by name in the source code.
|
|
///
|
|
/// ## Fails
|
|
/// 1. No symbol with the given name exists,
|
|
pub fn has_symbol(&self, name: &str) -> SymbolTester {
|
|
SymbolTester::new_first_binding(self, self.build(), name)
|
|
}
|
|
|
|
/// Tests that a class with the given name exists
|
|
///
|
|
/// ## Fails
|
|
/// If no class with the given name exists.
|
|
pub fn has_class(&self, name: &str) -> ClassTester {
|
|
ClassTester::has_class(self.build(), name)
|
|
}
|
|
|
|
/// Check if semantic analysis produces an error matching `message`.
|
|
///
|
|
/// Matching is done using `.contains()`.
|
|
///
|
|
/// ## Fails
|
|
/// - If [`parsing`] produces errors
|
|
/// - If [`SemanticBuilder`] finishes without producing any errors.
|
|
/// - If [`SemanticBuilder`] finishes with errors, but none of them match `message`
|
|
///
|
|
/// [`parsing`]: oxc_parser::Parser::parse
|
|
pub fn has_error(&self, message: &str) {
|
|
let SemanticBuilderReturn { errors, .. } = self.build_with_errors();
|
|
assert!(
|
|
!errors.is_empty(),
|
|
"Expected an error matching '{message}', but no errors were produced"
|
|
);
|
|
if errors.iter().any(|e| e.message.contains(message)) {
|
|
return;
|
|
}
|
|
|
|
let num_errors = errors.len();
|
|
let rendered_errors =
|
|
self.wrap_diagnostics(errors).into_iter().map(|e| e.to_string()).join("\n\n");
|
|
|
|
panic!(
|
|
"Expected an error containing '{message}', but none of the {num_errors} matched:\n\n{rendered_errors}",
|
|
)
|
|
}
|
|
|
|
/// 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<OxcDiagnostic>) -> 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()
|
|
}
|
|
}
|