diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index bdbfe38..f5fec30 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -373,23 +373,27 @@ impl Compile for AttrSet { } if !self.dyns.is_empty() { - 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); - let span = val_expr.span(); - let val = if stack_trace_enabled { - let span = encode_span(span, ctx); - format!( - "Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))", - span, val - ) - } else { - val - }; - let dyn_span_str = encode_span(*attr_span, ctx); - (key, val, dyn_span_str) - }).multiunzip::<(Vec<_>, Vec<_>, Vec<_>)>(); + 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); + let span = val_expr.span(); + let val = if stack_trace_enabled { + let span = encode_span(span, ctx); + format!( + "Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))", + span, val + ) + } else { + val + }; + let dyn_span_str = encode_span(*attr_span, ctx); + (key, val, dyn_span_str) + }) + .multiunzip::<(Vec<_>, Vec<_>, Vec<_>)>(); format!( "Nix.mkAttrsWithPos({{{}}},{{{}}},{{dynKeys:[{}],dynVals:[{}],dynSpans:[{}]}})", attrs.join(","), @@ -399,11 +403,14 @@ impl Compile 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(",")) } - } } diff --git a/nix-js/src/error.rs b/nix-js/src/error.rs index bde6e49..ba977f3 100644 --- a/nix-js/src/error.rs +++ b/nix-js/src/error.rs @@ -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, + #[related] + stack_trace: Vec, }, #[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>, } -pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec { +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, 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 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 { 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 ctx.get_source(id), Err(_) => continue, }; @@ -241,11 +305,7 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec Vec) -> Vec { + 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() +} diff --git a/nix-js/src/ir/downgrade.rs b/nix-js/src/ir/downgrade.rs index 686f5de..2c27d5b 100644 --- a/nix-js/src/ir/downgrade.rs +++ b/nix-js/src/ir/downgrade.rs @@ -105,14 +105,36 @@ impl Downgrade 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 } diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index b6ebaf3..e46ed3a 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -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, ) diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index d39d95d..f64da72 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -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 std::result::Result, NixError> { +fn op_read_dir( + #[string] path: String, +) -> std::result::Result, NixError> { let path = Path::new(&path); if !path.is_dir() { @@ -205,8 +205,13 @@ fn op_read_dir(#[string] path: String) -> std::result::Result Runtime { let global_value = self .js_runtime .execute_script("", 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 diff --git a/nix-js/tests/builtins.rs b/nix-js/tests/builtins.rs index ba0b11f..8d4e089 100644 --- a/nix-js/tests/builtins.rs +++ b/nix-js/tests/builtins.rs @@ -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())) + ); } diff --git a/nix-js/tests/derivation.rs b/nix-js/tests/derivation.rs index c489a42..612eb74 100644 --- a/nix-js/tests/derivation.rs +++ b/nix-js/tests/derivation.rs @@ -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") { diff --git a/nix-js/tests/io_operations.rs b/nix-js/tests/io_operations.rs index 7b90f38..c5a5106 100644 --- a/nix-js/tests/io_operations.rs +++ b/nix-js/tests/io_operations.rs @@ -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")); } -