feat(error): stack trace
This commit is contained in:
@@ -373,7 +373,10 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !self.dyns.is_empty() {
|
if !self.dyns.is_empty() {
|
||||||
let (keys, vals, dyn_spans) = self.dyns.iter().map(|(key, val, attr_span)| {
|
let (keys, vals, dyn_spans) = self
|
||||||
|
.dyns
|
||||||
|
.iter()
|
||||||
|
.map(|(key, val, attr_span)| {
|
||||||
let key = ctx.get_ir(*key).compile(ctx);
|
let key = ctx.get_ir(*key).compile(ctx);
|
||||||
let val_expr = ctx.get_ir(*val);
|
let val_expr = ctx.get_ir(*val);
|
||||||
let val = val_expr.compile(ctx);
|
let val = val_expr.compile(ctx);
|
||||||
@@ -389,7 +392,8 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
|
|||||||
};
|
};
|
||||||
let dyn_span_str = encode_span(*attr_span, ctx);
|
let dyn_span_str = encode_span(*attr_span, ctx);
|
||||||
(key, val, dyn_span_str)
|
(key, val, dyn_span_str)
|
||||||
}).multiunzip::<(Vec<_>, Vec<_>, Vec<_>)>();
|
})
|
||||||
|
.multiunzip::<(Vec<_>, Vec<_>, Vec<_>)>();
|
||||||
format!(
|
format!(
|
||||||
"Nix.mkAttrsWithPos({{{}}},{{{}}},{{dynKeys:[{}],dynVals:[{}],dynSpans:[{}]}})",
|
"Nix.mkAttrsWithPos({{{}}},{{{}}},{{dynKeys:[{}],dynVals:[{}],dynSpans:[{}]}})",
|
||||||
attrs.join(","),
|
attrs.join(","),
|
||||||
@@ -399,11 +403,14 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
|
|||||||
dyn_spans.join(",")
|
dyn_spans.join(",")
|
||||||
)
|
)
|
||||||
} else if !attr_positions.is_empty() {
|
} else if !attr_positions.is_empty() {
|
||||||
format!("Nix.mkAttrsWithPos({{{}}},{{{}}})", attrs.join(","), attr_positions.join(","))
|
format!(
|
||||||
|
"Nix.mkAttrsWithPos({{{}}},{{{}}})",
|
||||||
|
attrs.join(","),
|
||||||
|
attr_positions.join(",")
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("{{{}}}", attrs.join(","))
|
format!("{{{}}}", attrs.join(","))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use deno_core::error::JsError;
|
||||||
|
use deno_error::JsErrorClass as _;
|
||||||
|
use itertools::Itertools as _;
|
||||||
use miette::{Diagnostic, NamedSource, SourceSpan};
|
use miette::{Diagnostic, NamedSource, SourceSpan};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -113,6 +116,8 @@ pub enum Error {
|
|||||||
message: String,
|
message: String,
|
||||||
#[help]
|
#[help]
|
||||||
js_backtrace: Option<String>,
|
js_backtrace: Option<String>,
|
||||||
|
#[related]
|
||||||
|
stack_trace: Vec<StackFrame>,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Internal error: {message}")]
|
#[error("Internal error: {message}")]
|
||||||
@@ -153,6 +158,7 @@ impl Error {
|
|||||||
span: None,
|
span: None,
|
||||||
message: msg,
|
message: msg,
|
||||||
js_backtrace: backtrace,
|
js_backtrace: backtrace,
|
||||||
|
stack_trace: Vec::new(),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
@@ -197,14 +203,72 @@ pub fn text_range_to_source_span(range: rnix::TextRange) -> SourceSpan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stack frame types from Nix evaluation
|
/// Stack frame types from Nix evaluation
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Error, Diagnostic)]
|
||||||
pub(crate) struct NixStackFrame {
|
#[error("{message}")]
|
||||||
pub span: rnix::TextRange,
|
pub struct StackFrame {
|
||||||
|
#[label]
|
||||||
|
pub span: SourceSpan,
|
||||||
|
#[help]
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub source: Source,
|
#[source_code]
|
||||||
|
pub src: NamedSource<Arc<str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<NixStackFrame> {
|
const MAX_STACK_FRAMES: usize = 10;
|
||||||
|
const FRAMES_AT_START: usize = 5;
|
||||||
|
const FRAMES_AT_END: usize = 5;
|
||||||
|
|
||||||
|
pub(crate) fn parse_js_error(error: Box<JsError>, ctx: &impl RuntimeContext) -> Error {
|
||||||
|
let (span, src, frames) = if let Some(stack) = &error.stack {
|
||||||
|
let mut frames = parse_frames(stack, ctx);
|
||||||
|
|
||||||
|
if let Some(last_frame) = frames.pop() {
|
||||||
|
(
|
||||||
|
Some(text_range_to_source_span(last_frame.span)),
|
||||||
|
Some(last_frame.src.into()),
|
||||||
|
frames,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(None, None, frames)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(None, None, Vec::new())
|
||||||
|
};
|
||||||
|
let stack_trace = truncate_stack_trace(frames);
|
||||||
|
let message = error.get_message().to_string();
|
||||||
|
let js_backtrace = error.stack.map(|stack| {
|
||||||
|
stack
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.starts_with("NIX_STACK_FRAME:"))
|
||||||
|
.join("\n")
|
||||||
|
});
|
||||||
|
|
||||||
|
Error::EvalError {
|
||||||
|
src,
|
||||||
|
span,
|
||||||
|
message,
|
||||||
|
js_backtrace,
|
||||||
|
stack_trace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NixStackFrame {
|
||||||
|
span: rnix::TextRange,
|
||||||
|
message: String,
|
||||||
|
src: Source,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NixStackFrame> for StackFrame {
|
||||||
|
fn from(NixStackFrame { span, message, src }: NixStackFrame) -> Self {
|
||||||
|
StackFrame {
|
||||||
|
span: text_range_to_source_span(span),
|
||||||
|
message,
|
||||||
|
src: src.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_frames(stack: &str, ctx: &impl RuntimeContext) -> Vec<NixStackFrame> {
|
||||||
let mut frames = Vec::new();
|
let mut frames = Vec::new();
|
||||||
|
|
||||||
for line in stack.lines() {
|
for line in stack.lines() {
|
||||||
@@ -218,7 +282,7 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let source = match parts[0].parse() {
|
let src = match parts[0].parse() {
|
||||||
Ok(id) => ctx.get_source(id),
|
Ok(id) => ctx.get_source(id),
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
@@ -241,11 +305,7 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
frames.push(NixStackFrame {
|
frames.push(NixStackFrame { span, message, src });
|
||||||
span,
|
|
||||||
message,
|
|
||||||
source,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduplicate consecutive identical frames
|
// Deduplicate consecutive identical frames
|
||||||
@@ -253,3 +313,34 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
|
|||||||
|
|
||||||
frames
|
frames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn truncate_stack_trace(frames: Vec<NixStackFrame>) -> Vec<StackFrame> {
|
||||||
|
let reversed: Vec<_> = frames.into_iter().rev().collect();
|
||||||
|
let total = reversed.len();
|
||||||
|
|
||||||
|
if total <= MAX_STACK_FRAMES {
|
||||||
|
return reversed.into_iter().map(Into::into).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let omitted_count = total - FRAMES_AT_START - FRAMES_AT_END;
|
||||||
|
|
||||||
|
reversed
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, frame)| {
|
||||||
|
if i < FRAMES_AT_START {
|
||||||
|
Some(frame.into())
|
||||||
|
} else if i == FRAMES_AT_START {
|
||||||
|
Some(StackFrame {
|
||||||
|
span: text_range_to_source_span(frame.span),
|
||||||
|
message: format!("... ({} more frames omitted)", omitted_count),
|
||||||
|
src: frame.src.into(),
|
||||||
|
})
|
||||||
|
} else if i >= total - FRAMES_AT_END {
|
||||||
|
Some(frame.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|||||||
@@ -105,14 +105,36 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
|
|||||||
let expr = if parts.len() == 1 {
|
let expr = if parts.len() == 1 {
|
||||||
let part = parts.into_iter().next().unwrap();
|
let part = parts.into_iter().next().unwrap();
|
||||||
if let &Ir::Str(Str { ref val, span }) = ctx.get_ir(part)
|
if let &Ir::Str(Str { ref val, span }) = ctx.get_ir(part)
|
||||||
&& let Some(path) = val.strip_prefix("<").map(|path| &path[..path.len() - 1]) {
|
&& let Some(path) = val.strip_prefix("<").map(|path| &path[..path.len() - 1])
|
||||||
ctx.replace_ir(part, Str { val: path.to_string(), span }.to_ir());
|
{
|
||||||
|
ctx.replace_ir(
|
||||||
|
part,
|
||||||
|
Str {
|
||||||
|
val: path.to_string(),
|
||||||
|
span,
|
||||||
|
}
|
||||||
|
.to_ir(),
|
||||||
|
);
|
||||||
let sym = ctx.new_sym("findFile".into());
|
let sym = ctx.new_sym("findFile".into());
|
||||||
let find_file = ctx.new_expr(Builtin { inner: sym, span }.to_ir());
|
let find_file = ctx.new_expr(Builtin { inner: sym, span }.to_ir());
|
||||||
let sym = ctx.new_sym("nixPath".into());
|
let sym = ctx.new_sym("nixPath".into());
|
||||||
let nix_path = ctx.new_expr(Builtin { inner: sym, span }.to_ir());
|
let nix_path = ctx.new_expr(Builtin { inner: sym, span }.to_ir());
|
||||||
let call = ctx.new_expr(Call { func: find_file, arg: nix_path, span }.to_ir());
|
let call = ctx.new_expr(
|
||||||
return Ok(ctx.new_expr(Call { func: call, arg: part, span }.to_ir()));
|
Call {
|
||||||
|
func: find_file,
|
||||||
|
arg: nix_path,
|
||||||
|
span,
|
||||||
|
}
|
||||||
|
.to_ir(),
|
||||||
|
);
|
||||||
|
return Ok(ctx.new_expr(
|
||||||
|
Call {
|
||||||
|
func: call,
|
||||||
|
arg: part,
|
||||||
|
span,
|
||||||
|
}
|
||||||
|
.to_ir(),
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
part
|
part
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -414,9 +414,10 @@ where
|
|||||||
.to_ir(),
|
.to_ir(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::internal(
|
return Err(Error::internal(format!(
|
||||||
format!("binding '{}' not found", format_symbol(ctx.get_sym(sym))),
|
"binding '{}' not found",
|
||||||
));
|
format_symbol(ctx.get_sym(sym))
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,7 +535,11 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(temp_attrs.stcs.into_iter().map(|(k, (v, _))| (k, v)).collect())
|
Ok(temp_attrs
|
||||||
|
.stcs
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, (v, _))| (k, v))
|
||||||
|
.collect())
|
||||||
},
|
},
|
||||||
body_fn,
|
body_fn,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ use std::path::{Component, Path, PathBuf};
|
|||||||
use std::sync::{Arc, Once};
|
use std::sync::{Arc, Once};
|
||||||
|
|
||||||
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8};
|
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8};
|
||||||
use deno_error::JsErrorClass;
|
|
||||||
use itertools::Itertools as _;
|
|
||||||
use rust_embed::Embed;
|
use rust_embed::Embed;
|
||||||
|
|
||||||
use crate::error::{Error, Result, Source};
|
use crate::error::{Error, Result, Source};
|
||||||
@@ -189,7 +187,9 @@ fn op_read_file_type(#[string] path: String) -> std::result::Result<String, NixE
|
|||||||
|
|
||||||
#[deno_core::op2]
|
#[deno_core::op2]
|
||||||
#[serde]
|
#[serde]
|
||||||
fn op_read_dir(#[string] path: String) -> std::result::Result<std::collections::HashMap<String, String>, NixError> {
|
fn op_read_dir(
|
||||||
|
#[string] path: String,
|
||||||
|
) -> std::result::Result<std::collections::HashMap<String, String>, NixError> {
|
||||||
let path = Path::new(&path);
|
let path = Path::new(&path);
|
||||||
|
|
||||||
if !path.is_dir() {
|
if !path.is_dir() {
|
||||||
@@ -205,8 +205,13 @@ fn op_read_dir(#[string] path: String) -> std::result::Result<std::collections::
|
|||||||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
||||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
let file_type = entry.file_type()
|
let file_type = entry.file_type().map_err(|e| {
|
||||||
.map_err(|e| format!("Failed to read file type for {}: {}", entry.path().display(), e))?;
|
format!(
|
||||||
|
"Failed to read file type for {}: {}",
|
||||||
|
entry.path().display(),
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let type_str = if file_type.is_dir() {
|
let type_str = if file_type.is_dir() {
|
||||||
"directory"
|
"directory"
|
||||||
@@ -584,41 +589,13 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
|
|||||||
let global_value = self
|
let global_value = self
|
||||||
.js_runtime
|
.js_runtime
|
||||||
.execute_script("<eval>", script)
|
.execute_script("<eval>", script)
|
||||||
.map_err(|e| {
|
.map_err(|error| {
|
||||||
// Get current source from Context
|
// Get current source from Context
|
||||||
let op_state = self.js_runtime.op_state();
|
let op_state = self.js_runtime.op_state();
|
||||||
let op_state_borrow = op_state.borrow();
|
let op_state_borrow = op_state.borrow();
|
||||||
let ctx: &Ctx = op_state_borrow.get_ctx();
|
let ctx: &Ctx = op_state_borrow.get_ctx();
|
||||||
|
|
||||||
let msg = e.get_message().to_string();
|
crate::error::parse_js_error(error, ctx)
|
||||||
let mut span = None;
|
|
||||||
let mut source = None;
|
|
||||||
|
|
||||||
// Parse Nix stack trace frames
|
|
||||||
if let Some(stack) = &e.stack {
|
|
||||||
let frames = crate::error::parse_nix_stack(stack, ctx);
|
|
||||||
|
|
||||||
if let Some(last_frame) = frames.last() {
|
|
||||||
span = Some(last_frame.span);
|
|
||||||
source = Some(last_frame.source.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let js_backtrace = e.stack.map(|stack| {
|
|
||||||
stack
|
|
||||||
.lines()
|
|
||||||
.filter(|line| !line.starts_with("NIX_STACK_FRAME:"))
|
|
||||||
.join("\n")
|
|
||||||
});
|
|
||||||
let mut error = Error::eval_error(msg.clone(), js_backtrace);
|
|
||||||
if let Some(span) = span {
|
|
||||||
error = error.with_span(span);
|
|
||||||
}
|
|
||||||
if let Some(source) = source {
|
|
||||||
error = error.with_source(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
error
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Retrieve scope from JsRuntime
|
// Retrieve scope from JsRuntime
|
||||||
|
|||||||
@@ -320,5 +320,8 @@ fn builtins_function_args() {
|
|||||||
fn builtins_parse_drv_name() {
|
fn builtins_parse_drv_name() {
|
||||||
let result = eval(r#"builtins.parseDrvName "nix-js-0.1.0pre""#).unwrap_attr_set();
|
let result = eval(r#"builtins.parseDrvName "nix-js-0.1.0pre""#).unwrap_attr_set();
|
||||||
assert_eq!(result.get("name"), Some(&Value::String("nix-js".into())));
|
assert_eq!(result.get("name"), Some(&Value::String("nix-js".into())));
|
||||||
assert_eq!(result.get("version"), Some(&Value::String("0.1.0pre".into())));
|
assert_eq!(
|
||||||
|
result.get("version"),
|
||||||
|
Some(&Value::String("0.1.0pre".into()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,7 +212,10 @@ fn multi_output_two_outputs() {
|
|||||||
"/nix/store/vmyjryfipkn9ss3ya23hk8p3m58l6dsl-multi.drv"
|
"/nix/store/vmyjryfipkn9ss3ya23hk8p3m58l6dsl-multi.drv"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
panic!("drvPath should be a string, got: {:?}", attrs.get("drvPath"));
|
panic!(
|
||||||
|
"drvPath should be a string, got: {:?}",
|
||||||
|
attrs.get("drvPath")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Value::String(out_path)) = attrs.get("outPath") {
|
if let Some(Value::String(out_path)) = attrs.get("outPath") {
|
||||||
|
|||||||
@@ -313,9 +313,18 @@ fn read_dir_basic() {
|
|||||||
let result = eval(&expr);
|
let result = eval(&expr);
|
||||||
|
|
||||||
if let Value::AttrSet(attrs) = result {
|
if let Value::AttrSet(attrs) = result {
|
||||||
assert_eq!(attrs.get("file1.txt"), Some(&Value::String("regular".to_string())));
|
assert_eq!(
|
||||||
assert_eq!(attrs.get("file2.txt"), Some(&Value::String("regular".to_string())));
|
attrs.get("file1.txt"),
|
||||||
assert_eq!(attrs.get("subdir"), Some(&Value::String("directory".to_string())));
|
Some(&Value::String("regular".to_string()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
attrs.get("file2.txt"),
|
||||||
|
Some(&Value::String("regular".to_string()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
attrs.get("subdir"),
|
||||||
|
Some(&Value::String("directory".to_string()))
|
||||||
|
);
|
||||||
assert_eq!(attrs.len(), 3);
|
assert_eq!(attrs.len(), 3);
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected AttrSet, got {:?}", result);
|
panic!("Expected AttrSet, got {:?}", result);
|
||||||
@@ -359,4 +368,3 @@ fn read_dir_on_file_fails() {
|
|||||||
let err_msg = result.unwrap_err().to_string();
|
let err_msg = result.unwrap_err().to_string();
|
||||||
assert!(err_msg.contains("not a directory"));
|
assert!(err_msg.contains("not a directory"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user