summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Whitaker <17734409+nathanwhit@users.noreply.github.com>2024-05-09 13:49:10 -0700
committerGitHub <noreply@github.com>2024-05-09 13:49:10 -0700
commit19c0633a947f2ee28cc07b59c32322151779101d (patch)
treeab7983bf954cd109517f80b42bd3199eb27efaa4
parent439b3b8db97106be44bb5fde9573632c20ed4e95 (diff)
refactor(lsp): Have JS drive TSC event loop in LSP (#23565)
-rw-r--r--cli/lsp/tsc.rs342
-rw-r--r--cli/tsc/99_main_compiler.js83
-rw-r--r--cli/util/mod.rs1
-rw-r--r--cli/util/result.rs16
-rw-r--r--cli/util/v8.rs2
-rw-r--r--cli/util/v8/convert.rs57
-rw-r--r--tests/integration/repl_tests.rs12
7 files changed, 333 insertions, 180 deletions
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
index fa35f63bd..a950ea0bb 100644
--- a/cli/lsp/tsc.rs
+++ b/cli/lsp/tsc.rs
@@ -8,6 +8,7 @@ use super::documents::DocumentsFilter;
use super::language_server;
use super::language_server::StateSnapshot;
use super::performance::Performance;
+use super::performance::PerformanceMark;
use super::refactor::RefactorCodeActionData;
use super::refactor::ALL_KNOWN_REFACTOR_ACTION_KINDS;
use super::refactor::EXTRACT_CONSTANT;
@@ -28,16 +29,19 @@ use crate::tsc::ResolveArgs;
use crate::tsc::MISSING_DEPENDENCY_SPECIFIER;
use crate::util::path::relative_specifier;
use crate::util::path::to_percent_decoded_str;
+use crate::util::result::InfallibleResultExt;
+use crate::util::v8::convert;
+use deno_core::convert::Smi;
+use deno_core::convert::ToV8;
+use deno_core::error::StdAnyError;
use deno_runtime::fs_util::specifier_to_file_path;
use dashmap::DashMap;
use deno_ast::MediaType;
use deno_core::anyhow::anyhow;
use deno_core::anyhow::Context as _;
-use deno_core::error::custom_error;
use deno_core::error::AnyError;
use deno_core::futures::FutureExt;
-use deno_core::located_script_name;
use deno_core::op2;
use deno_core::parking_lot::Mutex;
use deno_core::resolve_url;
@@ -63,9 +67,11 @@ use regex::Captures;
use regex::Regex;
use serde_repr::Deserialize_repr;
use serde_repr::Serialize_repr;
+use std::cell::RefCell;
use std::cmp;
use std::collections::HashMap;
use std::collections::HashSet;
+use std::convert::Infallible;
use std::net::SocketAddr;
use std::ops::Range;
use std::path::Path;
@@ -244,6 +250,16 @@ pub enum ChangeKind {
Closed = 2,
}
+impl<'a> ToV8<'a> for ChangeKind {
+ type Error = Infallible;
+ fn to_v8(
+ self,
+ scope: &mut v8::HandleScope<'a>,
+ ) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
+ Smi(self as u8).to_v8(scope)
+ }
+}
+
impl Serialize for ChangeKind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
@@ -260,15 +276,28 @@ pub struct PendingChange {
pub config_changed: bool,
}
-impl PendingChange {
- fn to_v8<'s>(
- &self,
- scope: &mut v8::HandleScope<'s>,
- ) -> Result<v8::Local<'s, v8::Value>, AnyError> {
- let modified_scripts = serde_v8::to_v8(scope, &self.modified_scripts)?;
+impl<'a> ToV8<'a> for PendingChange {
+ type Error = Infallible;
+ fn to_v8(
+ self,
+ scope: &mut v8::HandleScope<'a>,
+ ) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
+ let modified_scripts = {
+ let mut modified_scripts_v8 =
+ Vec::with_capacity(self.modified_scripts.len());
+ for (specifier, kind) in &self.modified_scripts {
+ let specifier = v8::String::new(scope, specifier).unwrap().into();
+ let kind = kind.to_v8(scope).unwrap_infallible();
+ let pair =
+ v8::Array::new_with_elements(scope, &[specifier, kind]).into();
+ modified_scripts_v8.push(pair);
+ }
+ v8::Array::new_with_elements(scope, &modified_scripts_v8).into()
+ };
let project_version =
v8::Integer::new_from_unsigned(scope, self.project_version as u32).into();
let config_changed = v8::Boolean::new(scope, self.config_changed).into();
+
Ok(
v8::Array::new_with_elements(
scope,
@@ -277,7 +306,9 @@ impl PendingChange {
.into(),
)
}
+}
+impl PendingChange {
fn coalesce(
&mut self,
new_version: usize,
@@ -1068,6 +1099,7 @@ impl TsServer {
let droppable_token = DroppableToken(token.clone());
let (tx, mut rx) = oneshot::channel::<Result<String, AnyError>>();
let change = self.pending_change.lock().take();
+
if self
.sender
.send((req, snapshot, tx, token.clone(), change))
@@ -3951,10 +3983,12 @@ struct State {
last_id: usize,
performance: Arc<Performance>,
// the response from JS, as a JSON string
- response: Option<String>,
+ response_tx: Option<oneshot::Sender<Result<String, AnyError>>>,
state_snapshot: Arc<StateSnapshot>,
specifier_map: Arc<TscSpecifierMap>,
token: CancellationToken,
+ pending_requests: Option<UnboundedReceiver<Request>>,
+ mark: Option<PerformanceMark>,
}
impl State {
@@ -3962,14 +3996,17 @@ impl State {
state_snapshot: Arc<StateSnapshot>,
specifier_map: Arc<TscSpecifierMap>,
performance: Arc<Performance>,
+ pending_requests: UnboundedReceiver<Request>,
) -> Self {
Self {
last_id: 1,
performance,
- response: None,
+ response_tx: None,
state_snapshot,
specifier_map,
token: Default::default(),
+ mark: None,
+ pending_requests: Some(pending_requests),
}
}
@@ -4090,6 +4127,75 @@ fn op_resolve(
op_resolve_inner(state, ResolveArgs { base, specifiers })
}
+struct TscRequestArray {
+ request: TscRequest,
+ id: Smi<usize>,
+ change: convert::OptionNull<PendingChange>,
+}
+
+impl<'a> ToV8<'a> for TscRequestArray {
+ type Error = StdAnyError;
+
+ fn to_v8(
+ self,
+ scope: &mut v8::HandleScope<'a>,
+ ) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
+ let id = self.id.to_v8(scope).unwrap_infallible();
+
+ let (method_name, args) = self.request.to_server_request(scope)?;
+
+ let method_name = deno_core::FastString::from_static(method_name)
+ .v8_string(scope)
+ .into();
+ let args = args.unwrap_or_else(|| v8::Array::new(scope, 0).into());
+
+ let change = self.change.to_v8(scope).unwrap_infallible();
+
+ Ok(
+ v8::Array::new_with_elements(scope, &[id, method_name, args, change])
+ .into(),
+ )
+ }
+}
+
+#[op2(async)]
+#[to_v8]
+async fn op_poll_requests(
+ state: Rc<RefCell<OpState>>,
+) -> convert::OptionNull<TscRequestArray> {
+ let mut pending_requests = {
+ let mut state = state.borrow_mut();
+ let state = state.try_borrow_mut::<State>().unwrap();
+ state.pending_requests.take().unwrap()
+ };
+
+ let Some((request, snapshot, response_tx, token, change)) =
+ pending_requests.recv().await
+ else {
+ return None.into();
+ };
+
+ let mut state = state.borrow_mut();
+ let state = state.try_borrow_mut::<State>().unwrap();
+ state.pending_requests = Some(pending_requests);
+ state.state_snapshot = snapshot;
+ state.token = token;
+ state.response_tx = Some(response_tx);
+ let id = state.last_id;
+ state.last_id += 1;
+ let mark = state
+ .performance
+ .mark_with_args(format!("tsc.host.{}", request.method()), &request);
+ state.mark = Some(mark);
+
+ Some(TscRequestArray {
+ request,
+ id: Smi(id),
+ change: change.into(),
+ })
+ .into()
+}
+
#[inline]
fn op_resolve_inner(
state: &mut OpState,
@@ -4118,9 +4224,25 @@ fn op_resolve_inner(
}
#[op2(fast)]
-fn op_respond(state: &mut OpState, #[string] response: String) {
+fn op_respond(
+ state: &mut OpState,
+ #[string] response: String,
+ #[string] error: String,
+) {
let state = state.borrow_mut::<State>();
- state.response = Some(response);
+ state.performance.measure(state.mark.take().unwrap());
+ let response = if !error.is_empty() {
+ Err(anyhow!("tsc error: {error}"))
+ } else {
+ Ok(response)
+ };
+
+ let was_sent = state.response_tx.take().unwrap().send(response).is_ok();
+ // Don't print the send error if the token is cancelled, it's expected
+ // to fail in that case and this commonly occurs.
+ if !was_sent && !state.token.is_cancelled() {
+ lsp_warn!("Unable to send result to client.");
+ }
}
#[op2]
@@ -4216,121 +4338,36 @@ fn op_project_version(state: &mut OpState) -> usize {
struct TscRuntime {
js_runtime: JsRuntime,
- server_request_fn_global: v8::Global<v8::Function>,
+ server_main_loop_fn_global: v8::Global<v8::Function>,
}
impl TscRuntime {
fn new(mut js_runtime: JsRuntime) -> Self {
- let server_request_fn_global = {
+ let server_main_loop_fn_global = {
let context = js_runtime.main_context();
let scope = &mut js_runtime.handle_scope();
let context_local = v8::Local::new(scope, context);
let global_obj = context_local.global(scope);
- let server_request_fn_str =
- v8::String::new_external_onebyte_static(scope, b"serverRequest")
+ let server_main_loop_fn_str =
+ v8::String::new_external_onebyte_static(scope, b"serverMainLoop")
.unwrap();
- let server_request_fn = v8::Local::try_from(
- global_obj.get(scope, server_request_fn_str.into()).unwrap(),
+ let server_main_loop_fn = v8::Local::try_from(
+ global_obj
+ .get(scope, server_main_loop_fn_str.into())
+ .unwrap(),
)
.unwrap();
- v8::Global::new(scope, server_request_fn)
+ v8::Global::new(scope, server_main_loop_fn)
};
Self {
- server_request_fn_global,
+ server_main_loop_fn_global,
js_runtime,
}
}
-
- /// Send a request into the runtime and return the JSON string containing the response.
- fn request(
- &mut self,
- state_snapshot: Arc<StateSnapshot>,
- request: TscRequest,
- change: Option<PendingChange>,
- token: CancellationToken,
- ) -> Result<String, AnyError> {
- if token.is_cancelled() {
- return Err(anyhow!("Operation was cancelled."));
- }
- let (performance, id) = {
- let op_state = self.js_runtime.op_state();
- let mut op_state = op_state.borrow_mut();
- let state = op_state.borrow_mut::<State>();
- state.state_snapshot = state_snapshot;
- state.token = token;
- state.last_id += 1;
- let id = state.last_id;
- (state.performance.clone(), id)
- };
- let mark = performance
- .mark_with_args(format!("tsc.host.{}", request.method()), &request);
-
- {
- let scope = &mut self.js_runtime.handle_scope();
- let tc_scope = &mut v8::TryCatch::new(scope);
- let server_request_fn =
- v8::Local::new(tc_scope, &self.server_request_fn_global);
- let undefined = v8::undefined(tc_scope).into();
-
- let change = if let Some(change) = change {
- change.to_v8(tc_scope)?
- } else {
- v8::null(tc_scope).into()
- };
-
- let (method, req_args) = request.to_server_request(tc_scope)?;
- let args = vec![
- v8::Integer::new(tc_scope, id as i32).into(),
- v8::String::new(tc_scope, method).unwrap().into(),
- req_args.unwrap_or_else(|| v8::Array::new(tc_scope, 0).into()),
- change,
- ];
-
- server_request_fn.call(tc_scope, undefined, &args);
- if tc_scope.has_caught() && !tc_scope.has_terminated() {
- if let Some(stack_trace) = tc_scope.stack_trace() {
- lsp_warn!(
- "Error during TS request \"{method}\":\n {}",
- stack_trace.to_rust_string_lossy(tc_scope),
- );
- } else if let Some(message) = tc_scope.message() {
- lsp_warn!(
- "Error during TS request \"{method}\":\n {}\n {}",
- message.get(tc_scope).to_rust_string_lossy(tc_scope),
- tc_scope
- .exception()
- .map(|exc| exc.to_rust_string_lossy(tc_scope))
- .unwrap_or_default()
- );
- } else {
- lsp_warn!(
- "Error during TS request \"{method}\":\n {}",
- tc_scope
- .exception()
- .map(|exc| exc.to_rust_string_lossy(tc_scope))
- .unwrap_or_default(),
- );
- }
- tc_scope.rethrow();
- }
- }
-
- let op_state = self.js_runtime.op_state();
- let mut op_state = op_state.borrow_mut();
- let state = op_state.borrow_mut::<State>();
-
- performance.measure(mark);
- state.response.take().ok_or_else(|| {
- custom_error(
- "RequestError",
- "The response was not received for the request.",
- )
- })
- }
}
fn run_tsc_thread(
- mut request_rx: UnboundedReceiver<Request>,
+ request_rx: UnboundedReceiver<Request>,
performance: Arc<Performance>,
specifier_map: Arc<TscSpecifierMap>,
maybe_inspector_server: Option<Arc<InspectorServer>>,
@@ -4340,9 +4377,13 @@ fn run_tsc_thread(
// supplied snapshot is an isolate that contains the TypeScript language
// server.
let mut tsc_runtime = JsRuntime::new(RuntimeOptions {
- extensions: vec![deno_tsc::init_ops(performance, specifier_map)],
+ extensions: vec![deno_tsc::init_ops(
+ performance,
+ specifier_map,
+ request_rx,
+ )],
startup_snapshot: Some(tsc::compiler_snapshot()),
- inspector: maybe_inspector_server.is_some(),
+ inspector: has_inspector_server,
..Default::default()
});
@@ -4355,40 +4396,53 @@ fn run_tsc_thread(
}
let tsc_future = async {
- start_tsc(&mut tsc_runtime, false).unwrap();
- let (request_signal_tx, mut request_signal_rx) = mpsc::unbounded_channel::<()>();
- let tsc_runtime = Rc::new(tokio::sync::Mutex::new(TscRuntime::new(tsc_runtime)));
+ // start_tsc(&mut tsc_runtime, false).unwrap();
+ let tsc_runtime =
+ Rc::new(tokio::sync::Mutex::new(TscRuntime::new(tsc_runtime)));
let tsc_runtime_ = tsc_runtime.clone();
+
let event_loop_fut = async {
loop {
- if has_inspector_server {
- tsc_runtime_.lock().await.js_runtime.run_event_loop(PollEventLoopOptions {
+ if let Err(e) = tsc_runtime_
+ .lock()
+ .await
+ .js_runtime
+ .run_event_loop(PollEventLoopOptions {
wait_for_inspector: false,
pump_v8_message_loop: true,
- }).await.ok();
+ })
+ .await
+ {
+ log::error!("Error in TSC event loop: {e}");
}
- request_signal_rx.recv_many(&mut vec![], 1000).await;
}
};
- tokio::pin!(event_loop_fut);
- loop {
- tokio::select! {
- biased;
- (maybe_request, mut tsc_runtime) = async { (request_rx.recv().await, tsc_runtime.lock().await) } => {
- if let Some((req, state_snapshot, tx, token, pending_change)) = maybe_request {
- let value = tsc_runtime.request(state_snapshot, req, pending_change, token.clone());
- request_signal_tx.send(()).unwrap();
- let was_sent = tx.send(value).is_ok();
- // Don't print the send error if the token is cancelled, it's expected
- // to fail in that case and this commonly occurs.
- if !was_sent && !token.is_cancelled() {
- lsp_warn!("Unable to send result to client.");
- }
- } else {
- break;
- }
- },
- _ = &mut event_loop_fut => {}
+ let main_loop_fut = {
+ let enable_debug = std::env::var("DENO_TSC_DEBUG")
+ .map(|s| {
+ let s = s.trim();
+ s == "1" || s.eq_ignore_ascii_case("true")
+ })
+ .unwrap_or(false);
+ let mut runtime = tsc_runtime.lock().await;
+ let main_loop = runtime.server_main_loop_fn_global.clone();
+ let args = {
+ let scope = &mut runtime.js_runtime.handle_scope();
+ let enable_debug_local =
+ v8::Local::<v8::Value>::from(v8::Boolean::new(scope, enable_debug));
+ [v8::Global::new(scope, enable_debug_local)]
+ };
+
+ runtime.js_runtime.call_with_args(&main_loop, &args)
+ };
+
+ tokio::select! {
+ biased;
+ _ = event_loop_fut => {},
+ res = main_loop_fut => {
+ if let Err(err) = res {
+ log::error!("Error in TSC main loop: {err}");
+ }
}
}
}
@@ -4410,30 +4464,23 @@ deno_core::extension!(deno_tsc,
op_script_version,
op_ts_config,
op_project_version,
+ op_poll_requests,
],
options = {
performance: Arc<Performance>,
specifier_map: Arc<TscSpecifierMap>,
+ request_rx: UnboundedReceiver<Request>,
},
state = |state, options| {
state.put(State::new(
Default::default(),
options.specifier_map,
options.performance,
+ options.request_rx,
));
},
);
-/// Instruct a language server runtime to start the language server and provide
-/// it with a minimal bootstrap configuration.
-fn start_tsc(runtime: &mut JsRuntime, debug: bool) -> Result<(), AnyError> {
- let init_config = json!({ "debug": debug });
- let init_src = format!("globalThis.serverInit({init_config});");
-
- runtime.execute_script(located_script_name!(), init_src)?;
- Ok(())
-}
-
#[derive(Debug, Deserialize_repr, Serialize_repr)]
#[repr(u32)]
pub enum CompletionTriggerKind {
@@ -5123,8 +5170,9 @@ mod tests {
}
fn setup_op_state(state_snapshot: Arc<StateSnapshot>) -> OpState {
+ let (_tx, rx) = mpsc::unbounded_channel();
let state =
- State::new(state_snapshot, Default::default(), Default::default());
+ State::new(state_snapshot, Default::default(), Default::default(), rx);
let mut op_state = OpState::new(None);
op_state.put(state);
op_state
diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js
index ddbd77ae0..16e8f1ee9 100644
--- a/cli/tsc/99_main_compiler.js
+++ b/cli/tsc/99_main_compiler.js
@@ -1079,19 +1079,69 @@ delete Object.prototype.__proto__;
/**
* @param {number} _id
* @param {any} data
+ * @param {any | null} error
*/
// TODO(bartlomieju): this feels needlessly generic, both type chcking
// and language server use it with inefficient serialization. Id is not used
// anyway...
- function respond(_id, data = null) {
- ops.op_respond(JSON.stringify(data));
+ function respond(_id, data = null, error = null) {
+ if (error) {
+ ops.op_respond(
+ "error",
+ "stack" in error ? error.stack.toString() : error.toString(),
+ );
+ } else {
+ ops.op_respond(JSON.stringify(data), "");
+ }
+ }
+
+ /** @typedef {[[string, number][], number, boolean] } PendingChange */
+ /**
+ * @template T
+ * @typedef {T | null} Option<T> */
+
+ /** @returns {Promise<[number, string, any[], Option<PendingChange>] | null>} */
+ async function pollRequests() {
+ return await ops.op_poll_requests();
+ }
+
+ let hasStarted = false;
+
+ /** @param {boolean} enableDebugLogging */
+ async function serverMainLoop(enableDebugLogging) {
+ if (hasStarted) {
+ throw new Error("The language server has already been initialized.");
+ }
+ hasStarted = true;
+ languageService = ts.createLanguageService(host, documentRegistry);
+ setLogDebug(enableDebugLogging, "TSLS");
+ debug("serverInit()");
+
+ while (true) {
+ const request = await pollRequests();
+ if (request === null) {
+ break;
+ }
+ try {
+ serverRequest(request[0], request[1], request[2], request[3]);
+ } catch (err) {
+ const reqString = "[" + request.map((v) =>
+ JSON.stringify(v)
+ ).join(", ") + "]";
+ error(
+ `Error occurred processing request ${reqString} : ${
+ "stack" in err ? err.stack : err
+ }`,
+ );
+ }
+ }
}
/**
* @param {number} id
* @param {string} method
* @param {any[]} args
- * @param {[[string, number][], number, boolean] | null} maybeChange
+ * @param {PendingChange | null} maybeChange
*/
function serverRequest(id, method, args, maybeChange) {
if (logDebug) {
@@ -1160,11 +1210,7 @@ delete Object.prototype.__proto__;
if (
!isCancellationError(e)
) {
- if ("stack" in e) {
- error(e.stack);
- } else {
- error(e);
- }
+ respond(id, {}, e);
throw e;
}
return respond(id, {});
@@ -1181,11 +1227,7 @@ delete Object.prototype.__proto__;
return respond(id, languageService[method](...args));
} catch (e) {
if (!isCancellationError(e)) {
- if ("stack" in e) {
- error(e.stack);
- } else {
- error(e);
- }
+ respond(id, null, e);
throw e;
}
return respond(id);
@@ -1198,18 +1240,6 @@ delete Object.prototype.__proto__;
}
}
- let hasStarted = false;
- /** @param {{ debug: boolean; }} init */
- function serverInit({ debug: debugFlag }) {
- if (hasStarted) {
- throw new Error("The language server has already been initialized.");
- }
- hasStarted = true;
- languageService = ts.createLanguageService(host, documentRegistry);
- setLogDebug(debugFlag, "TSLS");
- debug("serverInit()");
- }
-
// A build time only op that provides some setup information that is used to
// ensure the snapshot is setup properly.
/** @type {{ buildSpecifier: string; libs: string[]; nodeBuiltInModuleNames: string[] }} */
@@ -1300,6 +1330,5 @@ delete Object.prototype.__proto__;
// exposes the functions that are called when the compiler is used as a
// language service.
- global.serverInit = serverInit;
- global.serverRequest = serverRequest;
+ global.serverMainLoop = serverMainLoop;
})(this);
diff --git a/cli/util/mod.rs b/cli/util/mod.rs
index 7e0e1bd37..c8155dc51 100644
--- a/cli/util/mod.rs
+++ b/cli/util/mod.rs
@@ -12,6 +12,7 @@ pub mod gitignore;
pub mod logger;
pub mod path;
pub mod progress_bar;
+pub mod result;
pub mod sync;
pub mod text_encoding;
pub mod time;
diff --git a/cli/util/result.rs b/cli/util/result.rs
new file mode 100644
index 000000000..3203d04eb
--- /dev/null
+++ b/cli/util/result.rs
@@ -0,0 +1,16 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+use std::convert::Infallible;
+
+pub trait InfallibleResultExt<T> {
+ fn unwrap_infallible(self) -> T;
+}
+
+impl<T> InfallibleResultExt<T> for Result<T, Infallible> {
+ fn unwrap_infallible(self) -> T {
+ match self {
+ Ok(value) => value,
+ Err(never) => match never {},
+ }
+ }
+}
diff --git a/cli/util/v8.rs b/cli/util/v8.rs
index a8ab2c3d0..fb16e67b7 100644
--- a/cli/util/v8.rs
+++ b/cli/util/v8.rs
@@ -1,5 +1,7 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+pub mod convert;
+
#[inline(always)]
pub fn get_v8_flags_from_env() -> Vec<String> {
std::env::var("DENO_V8_FLAGS")
diff --git a/cli/util/v8/convert.rs b/cli/util/v8/convert.rs
new file mode 100644
index 000000000..28107d901
--- /dev/null
+++ b/cli/util/v8/convert.rs
@@ -0,0 +1,57 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+use deno_core::v8;
+use deno_core::FromV8;
+use deno_core::ToV8;
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+/// A wrapper type for `Option<T>` that (de)serializes `None` as `null`
+#[repr(transparent)]
+pub struct OptionNull<T>(pub Option<T>);
+
+impl<T> From<Option<T>> for OptionNull<T> {
+ fn from(option: Option<T>) -> Self {
+ Self(option)
+ }
+}
+
+impl<T> From<OptionNull<T>> for Option<T> {
+ fn from(value: OptionNull<T>) -> Self {
+ value.0
+ }
+}
+
+impl<'a, T> ToV8<'a> for OptionNull<T>
+where
+ T: ToV8<'a>,
+{
+ type Error = T::Error;
+
+ fn to_v8(
+ self,
+ scope: &mut v8::HandleScope<'a>,
+ ) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
+ match self.0 {
+ Some(value) => value.to_v8(scope),
+ None => Ok(v8::null(scope).into()),
+ }
+ }
+}
+
+impl<'a, T> FromV8<'a> for OptionNull<T>
+where
+ T: FromV8<'a>,
+{
+ type Error = T::Error;
+
+ fn from_v8(
+ scope: &mut v8::HandleScope<'a>,
+ value: v8::Local<'a, v8::Value>,
+ ) -> Result<Self, Self::Error> {
+ if value.is_null() {
+ Ok(OptionNull(None))
+ } else {
+ T::from_v8(scope, value).map(|v| OptionNull(Some(v)))
+ }
+ }
+}
diff --git a/tests/integration/repl_tests.rs b/tests/integration/repl_tests.rs
index 4dc6ab44c..cdcab8e18 100644
--- a/tests/integration/repl_tests.rs
+++ b/tests/integration/repl_tests.rs
@@ -895,7 +895,7 @@ fn repl_with_quiet_flag() {
assert!(!out.contains("Deno"));
assert!(!out.contains("exit using ctrl+d, ctrl+c, or close()"));
assert_ends_with!(out, "\"done\"\n");
- assert!(err.is_empty());
+ assert!(err.is_empty(), "Error: {}", err);
}
#[test]
@@ -959,7 +959,7 @@ fn npm_packages() {
);
assert_contains!(out, "hello");
- assert!(err.is_empty());
+ assert!(err.is_empty(), "Error: {}", err);
}
{
@@ -975,7 +975,7 @@ fn npm_packages() {
);
assert_contains!(out, "hello");
- assert!(err.is_empty());
+ assert!(err.is_empty(), "Error: {}", err);
}
{
@@ -989,7 +989,7 @@ fn npm_packages() {
assert_contains!(out, "[Module: null prototype] {");
assert_contains!(out, "Chalk: [class Chalk],");
- assert!(err.is_empty());
+ assert!(err.is_empty(), "Error: {}", err);
}
{
@@ -1005,7 +1005,7 @@ fn npm_packages() {
out,
"error: npm package 'asdfawe52345asdf' does not exist"
);
- assert!(err.is_empty());
+ assert!(err.is_empty(), "Error: {}", err);
}
{
@@ -1021,7 +1021,7 @@ fn npm_packages() {
);
assert_contains!(out, "no");
- assert!(err.is_empty());
+ assert!(err.is_empty(), "Error: {}", err);
}
}