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() {
|
||||
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 val_expr = ctx.get_ir(*val);
|
||||
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);
|
||||
(key, val, dyn_span_str)
|
||||
}).multiunzip::<(Vec<_>, Vec<_>, Vec<_>)>();
|
||||
})
|
||||
.multiunzip::<(Vec<_>, Vec<_>, Vec<_>)>();
|
||||
format!(
|
||||
"Nix.mkAttrsWithPos({{{}}},{{{}}},{{dynKeys:[{}],dynVals:[{}],dynSpans:[{}]}})",
|
||||
attrs.join(","),
|
||||
@@ -399,11 +403,14 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
|
||||
dyn_spans.join(",")
|
||||
)
|
||||
} else if !attr_positions.is_empty() {
|
||||
format!("Nix.mkAttrsWithPos({{{}}},{{{}}})", attrs.join(","), attr_positions.join(","))
|
||||
format!(
|
||||
"Nix.mkAttrsWithPos({{{}}},{{{}}})",
|
||||
attrs.join(","),
|
||||
attr_positions.join(",")
|
||||
)
|
||||
} else {
|
||||
format!("{{{}}}", attrs.join(","))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use deno_core::error::JsError;
|
||||
use deno_error::JsErrorClass as _;
|
||||
use itertools::Itertools as _;
|
||||
use miette::{Diagnostic, NamedSource, SourceSpan};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -113,6 +116,8 @@ pub enum Error {
|
||||
message: String,
|
||||
#[help]
|
||||
js_backtrace: Option<String>,
|
||||
#[related]
|
||||
stack_trace: Vec<StackFrame>,
|
||||
},
|
||||
|
||||
#[error("Internal error: {message}")]
|
||||
@@ -153,6 +158,7 @@ impl Error {
|
||||
span: None,
|
||||
message: msg,
|
||||
js_backtrace: backtrace,
|
||||
stack_trace: Vec::new(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -197,14 +203,72 @@ pub fn text_range_to_source_span(range: rnix::TextRange) -> SourceSpan {
|
||||
}
|
||||
|
||||
/// Stack frame types from Nix evaluation
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct NixStackFrame {
|
||||
pub span: rnix::TextRange,
|
||||
#[derive(Debug, Clone, Error, Diagnostic)]
|
||||
#[error("{message}")]
|
||||
pub struct StackFrame {
|
||||
#[label]
|
||||
pub span: SourceSpan,
|
||||
#[help]
|
||||
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();
|
||||
|
||||
for line in stack.lines() {
|
||||
@@ -218,7 +282,7 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
|
||||
continue;
|
||||
}
|
||||
|
||||
let source = match parts[0].parse() {
|
||||
let src = match parts[0].parse() {
|
||||
Ok(id) => ctx.get_source(id),
|
||||
Err(_) => continue,
|
||||
};
|
||||
@@ -241,11 +305,7 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
|
||||
}
|
||||
};
|
||||
|
||||
frames.push(NixStackFrame {
|
||||
span,
|
||||
message,
|
||||
source,
|
||||
});
|
||||
frames.push(NixStackFrame { span, message, src });
|
||||
}
|
||||
|
||||
// Deduplicate consecutive identical frames
|
||||
@@ -253,3 +313,34 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
|
||||
|
||||
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 part = parts.into_iter().next().unwrap();
|
||||
if let &Ir::Str(Str { ref val, span }) = ctx.get_ir(part)
|
||||
&& let Some(path) = val.strip_prefix("<").map(|path| &path[..path.len() - 1]) {
|
||||
ctx.replace_ir(part, Str { val: path.to_string(), span }.to_ir());
|
||||
&& let Some(path) = val.strip_prefix("<").map(|path| &path[..path.len() - 1])
|
||||
{
|
||||
ctx.replace_ir(
|
||||
part,
|
||||
Str {
|
||||
val: path.to_string(),
|
||||
span,
|
||||
}
|
||||
.to_ir(),
|
||||
);
|
||||
let sym = ctx.new_sym("findFile".into());
|
||||
let find_file = ctx.new_expr(Builtin { inner: sym, span }.to_ir());
|
||||
let sym = ctx.new_sym("nixPath".into());
|
||||
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());
|
||||
return Ok(ctx.new_expr(Call { func: call, arg: part, span }.to_ir()));
|
||||
let call = ctx.new_expr(
|
||||
Call {
|
||||
func: find_file,
|
||||
arg: nix_path,
|
||||
span,
|
||||
}
|
||||
.to_ir(),
|
||||
);
|
||||
return Ok(ctx.new_expr(
|
||||
Call {
|
||||
func: call,
|
||||
arg: part,
|
||||
span,
|
||||
}
|
||||
.to_ir(),
|
||||
));
|
||||
} else {
|
||||
part
|
||||
}
|
||||
|
||||
@@ -414,9 +414,10 @@ where
|
||||
.to_ir(),
|
||||
);
|
||||
} else {
|
||||
return Err(Error::internal(
|
||||
format!("binding '{}' not found", format_symbol(ctx.get_sym(sym))),
|
||||
));
|
||||
return Err(Error::internal(format!(
|
||||
"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,
|
||||
)
|
||||
|
||||
@@ -4,8 +4,6 @@ use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::{Arc, Once};
|
||||
|
||||
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8};
|
||||
use deno_error::JsErrorClass;
|
||||
use itertools::Itertools as _;
|
||||
use rust_embed::Embed;
|
||||
|
||||
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]
|
||||
#[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);
|
||||
|
||||
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 file_name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
let file_type = entry.file_type()
|
||||
.map_err(|e| format!("Failed to read file type for {}: {}", entry.path().display(), e))?;
|
||||
let file_type = entry.file_type().map_err(|e| {
|
||||
format!(
|
||||
"Failed to read file type for {}: {}",
|
||||
entry.path().display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
let type_str = if file_type.is_dir() {
|
||||
"directory"
|
||||
@@ -584,41 +589,13 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
|
||||
let global_value = self
|
||||
.js_runtime
|
||||
.execute_script("<eval>", script)
|
||||
.map_err(|e| {
|
||||
.map_err(|error| {
|
||||
// Get current source from Context
|
||||
let op_state = self.js_runtime.op_state();
|
||||
let op_state_borrow = op_state.borrow();
|
||||
let ctx: &Ctx = op_state_borrow.get_ctx();
|
||||
|
||||
let msg = e.get_message().to_string();
|
||||
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
|
||||
crate::error::parse_js_error(error, ctx)
|
||||
})?;
|
||||
|
||||
// Retrieve scope from JsRuntime
|
||||
|
||||
@@ -320,5 +320,8 @@ fn builtins_function_args() {
|
||||
fn builtins_parse_drv_name() {
|
||||
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("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"
|
||||
);
|
||||
} 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") {
|
||||
|
||||
@@ -313,9 +313,18 @@ fn read_dir_basic() {
|
||||
let result = eval(&expr);
|
||||
|
||||
if let Value::AttrSet(attrs) = result {
|
||||
assert_eq!(attrs.get("file1.txt"), 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.get("file1.txt"),
|
||||
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);
|
||||
} else {
|
||||
panic!("Expected AttrSet, got {:?}", result);
|
||||
@@ -359,4 +368,3 @@ fn read_dir_on_file_fails() {
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
assert!(err_msg.contains("not a directory"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user