Files
oxc-project__oxc/crates/oxc_semantic/tests/integration/util/mod.rs
Boshen 777f02ae10 refactor(diagnostics)!: return a Diagnostics type instead of Vec<OxcDiagnostic> (#23217)
## Summary

Introduces `oxc_diagnostics::Diagnostics` — a `Vec<OxcDiagnostic>` newtype that knows the difference between errors and warnings — and makes it the standard diagnostics return value across oxc.

- **New type**: `Diagnostics` with `has_errors()`, `has_warnings()`, `errors()`, `warnings()`, plus `Vec`-like ergonomics (Deref, `IntoIterator`, `Extend`, `From`/`Into<Vec>`) so most call sites are unchanged.
- **Return types**: `ParserReturn`, `SemanticBuilderReturn`, `IsolatedDeclarationsReturn`, the React Compiler `TransformResult`/`LintResult`, and `TransformerReturn` now expose `diagnostics: Diagnostics` instead of `errors: Vec<OxcDiagnostic>` (the React Compiler's separate `errors`/`warnings` are merged into one severity-tagged list).
- **Behaviour**: a React Compiler **error** now stops the transform (no output), while **warnings** flow through and the transform continues. `compile()` aborts codegen only when `diagnostics.has_errors()` — warnings alone no longer abort (previously any diagnostic did).

## Breaking change

Consumers reading `*.errors` on these return values must read `*.diagnostics`. React Compiler consumers that read `.errors`/`.warnings` separately should use `diagnostics.has_errors()` / `diagnostics.errors()` / `diagnostics.warnings()`.

Parser/semantic/isolated-declarations fatality is otherwise unchanged. Builds on the React Compiler transform feature (#23201).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-10 14:46:52 +00:00

318 lines
10 KiB
Rust

use std::sync::Arc;
use itertools::Itertools;
use oxc_allocator::Allocator;
#[cfg(feature = "cfg")]
use oxc_cfg::DisplayDot;
use oxc_diagnostics::{Error, NamedSource, OxcDiagnostic};
#[cfg(feature = "cfg")]
use oxc_semantic::dot::DebugDot;
use oxc_semantic::{Semantic, SemanticBuilder, SemanticBuilderReturn};
use oxc_span::SourceType;
mod class_tester;
mod expect;
mod symbol_tester;
pub use class_tester::ClassTester;
pub use expect::Expect;
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,
/// Build a child ids for scope tree?
scope_tree_child_ids: 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::ts())
}
/// 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::mjs())
}
/// 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,
scope_tree_child_ids: 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
}
/// Mark the [`SourceType`] as a script.
pub fn with_script(mut self, yes: bool) -> Self {
self.source_type = self.source_type.with_script(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
}
pub fn with_scope_tree_child_ids(mut self, yes: bool) -> Self {
self.scope_tree_child_ids = 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
#[expect(unstable_name_collisions)]
pub fn build(&self) -> Semantic<'_> {
let semantic_ret = self.build_with_errors();
match (self.expect_errors, semantic_ret.diagnostics.is_empty()) {
(true, true) => panic!("Expected errors, but none were produced"),
(false, false) => panic!(
"Semantic analysis failed:\n\n{}\n\n{}",
self.source_text,
semantic_ret
.diagnostics
.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
///
#[expect(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.diagnostics.is_empty(),
"\n Failed to parse source:\n{}\n\n{}",
self.source_text,
parse
.diagnostics
.iter()
.map(|e| format!("{e}"))
.intersperse("\n\n".to_owned())
.collect::<String>()
);
SemanticBuilder::new_compiler()
.with_build_nodes(true)
.with_cfg(self.cfg)
.build(self.allocator.alloc(parse.program))
}
#[cfg(feature = "cfg")]
pub fn basic_blocks_count(&self) -> usize {
let built = self.build();
built.cfg().map_or(0, |cfg| cfg.basic_blocks.len())
}
#[cfg(not(feature = "cfg"))]
#[expect(clippy::unused_self, reason = "keep method signature consistent across cfg modes")]
pub fn basic_blocks_count(&self) -> usize {
0
}
#[cfg(feature = "cfg")]
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")
})
}
#[cfg(not(feature = "cfg"))]
#[expect(clippy::unused_self, reason = "keep method signature consistent across cfg modes")]
pub fn basic_blocks_printed(&self) -> String {
String::default()
}
#[cfg(feature = "cfg")]
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()))
}
#[cfg(not(feature = "cfg"))]
#[expect(clippy::unused_self, reason = "keep method signature consistent across cfg modes")]
pub fn cfg_dot_diagram(&self) -> String {
String::default()
}
/// 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 { diagnostics, .. } = self.build_with_errors();
assert!(
!diagnostics.is_empty(),
"Expected an error matching '{message}', but no errors were produced"
);
if diagnostics.iter().any(|e| e.message.contains(message)) {
return;
}
let num_errors = diagnostics.len();
let rendered_errors = self
.wrap_diagnostics(diagnostics.into())
.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()
}
}