use crate::{
error::{
AndaxError as AErr,
TbErr::{self, *},
},
fns as f,
};
use lazy_static::lazy_static;
use regex::Regex;
use rhai::{
packages::Package, plugin::*, Engine, EvalAltResult as RhaiE, NativeCallContext as Ctx, Scope,
};
use std::{io::BufRead, path::Path};
use tracing::{debug, error, instrument, trace, warn};
pub(crate) fn rf<T>(ctx: Ctx, res: color_eyre::Result<T>) -> Result<T, Box<RhaiE>>
where
T: rhai::Variant + Clone,
{
res.map_err(|err| {
Box::new(RhaiE::ErrorRuntime(
Dynamic::from(AErr::RustReport(
ctx.fn_name().into(),
ctx.source().unwrap_or("").into(),
std::rc::Rc::from(err),
)),
ctx.position(),
))
})
}
fn gen_en() -> (Engine, Scope<'static>) {
let mut sc = Scope::new();
sc.push("USER_AGENT", f::tsunagu::USER_AGENT);
sc.push("IS_WIN32", cfg!(windows));
let mut en = Engine::new();
en.register_global_module(exported_module!(f::io::ar).into())
.register_global_module(exported_module!(f::tsunagu::ar).into())
.register_global_module(exported_module!(f::kokoro::ar).into())
.register_static_module("anda::rpmbuild", exported_module!(f::build::ar).into())
.register_static_module("anda::cfg", exported_module!(f::cfg::ar).into())
.build_type::<f::tsunagu::Req>()
.build_type::<f::rpm::RPMSpec>()
.register_type::<anda_config::Manifest>()
.register_fn("find_key_for_value", anda_config::Manifest::find_key_for_value)
.register_fn("get_project", anda_config::Manifest::get_project);
rhai_fs::FilesystemPackage::new().register_into_engine(&mut en);
(en, sc)
}
fn _gemsg(nanitozo: &TbErr) -> String {
match nanitozo {
Report(o) => format!("From: {o:#}"),
Arb(o) => format!("Caused by: {o}"),
Rhai(o) => format!("Rhai: {o}"),
}
}
fn _gpos(p: Position) -> Option<(usize, usize)> {
p.line().map(|l| (l, p.position().unwrap_or(0)))
}
pub fn _tb_fb(p: &str, s: std::path::Display, nntz: TbErr) {
error!("{p}: {s} (no position data)\n{}", _gemsg(&nntz));
}
lazy_static! {
static ref WORD_REGEX: Regex = Regex::new(r"[A-Za-z_][A-Za-z0-9_]*").unwrap();
}
#[instrument(name = "traceback")]
pub fn _tb(proj: &str, scr: &Path, nanitozo: TbErr, pos: Position, rhai_fn: &str, fn_src: &str) {
if let Some((line, col)) = _gpos(pos) {
let f = std::fs::File::open(scr);
let scr = scr.display();
macro_rules! die {
($var:expr, $msg:expr) => {{
if let Err(e) = $var {
error!($msg, e);
return _tb_fb(proj, scr, nanitozo);
}
$var.unwrap()
}};
}
let f = die!(f, "{proj}: Cannot open `{scr}`: {}");
for (n, sl) in std::io::BufReader::new(f).lines().enumerate() {
if n != line - 1 {
continue;
}
let sl = die!(sl, "{proj}: Cannot read line: {}").replace('\t', " ");
let m = if let Some(x) = WORD_REGEX.find_at(sl.as_str(), col - 1) {
let r = x.range();
if r.start != col - 1 {
1
} else {
r.len()
}
} else {
1
};
let ln = line.to_string().len();
let lns = " ".repeat(ln);
let _l = "─".repeat(ln);
let _r = "─".repeat(sl.len() + 2);
let mut code = format!(
"─{_l}─┬{_r}\n {lns} │ {scr}:{line}:{col}\n─{_l}─┼{_r}\n {line} │ {sl}\n {lns} │ {}{}",
" ".repeat(col - 1),
"🭶".repeat(m)
);
if !rhai_fn.is_empty() {
code += &format!("\n {lns} └─═ When invoking: {rhai_fn}()");
}
if !fn_src.is_empty() {
code += &format!("\n {lns} └─═ Function source: {fn_src}");
}
code += &format!("\n {lns} └─═ {}", _gemsg(&nanitozo));
code += &hint(&sl, &lns, &nanitozo, rhai_fn).unwrap_or_default();
let c = code.matches('└').count();
if c > 0 {
code = code.replacen('└', "├", c - 1);
}
return error!("Script Exception —— {proj}\n{code}");
}
error!("{proj}: Non-existence exception at {scr}:{line}");
}
_tb_fb(proj, scr.display(), nanitozo)
}
pub fn errhdl(name: &str, scr: &Path, err: EvalAltResult) {
trace!("{name}: Generating traceback");
if let EvalAltResult::ErrorRuntime(ref run_err, pos) = err {
match run_err.clone().try_cast::<AErr>() {
Some(AErr::RustReport(rhai_fn, fn_src, oerr)) => {
return _tb(name, scr, Report(oerr), pos, rhai_fn.as_str(), fn_src.as_str());
}
Some(AErr::RustError(rhai_fn, fn_src, oerr)) => {
return _tb(name, scr, Arb(oerr), pos, rhai_fn.as_str(), fn_src.as_str());
}
Some(AErr::Exit(b)) => {
if b {
warn!("世界を壊している。\n{}", crate::error::EARTH);
error!("生存係為咗喵?打程式幾好呀。仲喵要咁憤世嫉俗喎。還掂おこちゃま戦争係政治家嘅事……");
trace!("あなたは世界の終わりにずんだを食べるのだ");
}
return debug!("Exit from rhai at: {pos}");
}
None => {}
}
}
trace!("Rhai moment: {err:#?}");
let pos = err.position();
_tb(name, scr, Rhai(err), pos, "", "");
}
pub fn run<'a>(
name: &'a str,
scr: &'a Path,
labels: std::collections::BTreeMap<String, String>,
f: impl FnOnce(&mut Scope<'a>),
) -> Option<Scope<'a>> {
let (en, mut sc) = gen_en();
f(&mut sc);
let mut lbls = rhai::Map::new();
for (k, v) in labels {
lbls.insert(k.into(), v.into());
}
sc.push("labels", lbls);
exec(name, scr, sc, en)
}
#[instrument(skip(sc, en))]
fn exec<'a>(name: &'a str, scr: &'a Path, mut sc: Scope<'a>, en: Engine) -> Option<Scope<'a>> {
debug!("Running {name}");
match en.run_file_with_scope(&mut sc, scr.to_path_buf()) {
Ok(()) => Some(sc),
Err(err) => {
errhdl(name, scr, *err);
None
}
}
}
#[instrument(skip(sl, lns, nanitozo, rhai_fn))]
fn hint(sl: &str, lns: &str, nanitozo: &TbErr, rhai_fn: &str) -> Option<String> {
trace!("Matching hints");
macro_rules! h {
($s:expr) => {
let left = " ".repeat(7 + lns.len());
let mut s = String::new();
let mut first = true;
for l in $s.lines() {
let l = l.trim();
if first {
s = format!("\n {lns} └─═ Hint: {l}");
first = false;
continue;
}
s += &format!("\n{left}...: {l}");
}
return Some(s);
};
}
match nanitozo {
Arb(err) => {
if let Some(err) = (**err).downcast_ref::<EvalAltResult>() {
return hint_ear(sl, lns, err, rhai_fn);
}
let s = format!("{err}");
if rhai_fn == "gh"
&& s.starts_with("https://api.github.com/repos/")
&& s.ends_with("/releases/latest: status code 404")
{
h!("Check if the repo is valid. Only releases are supported; use gh_tag() for tags.");
}
None
}
Report(report) => {
if let Some(err) = report.source() {
if let Some(err) = err.downcast_ref::<EvalAltResult>() {
return hint_ear(sl, lns, err, rhai_fn);
}
}
None
}
Rhai(err) => hint_ear(sl, lns, err, rhai_fn),
}
}
fn hint_ear(sl: &str, lns: &str, ear: &EvalAltResult, _rhai_fn: &str) -> Option<String> {
trace!("Hinting for EvalAltResult");
macro_rules! h {
($s:expr) => {
let left = " ".repeat(7 + lns.len());
let mut s = String::new();
let mut first = true;
for l in $s.lines() {
let l = l.trim();
if first {
s = format!("\n {lns} └─═ Hint: {l}");
first = false;
continue;
}
s += &format!("\n{left}...: {l}");
}
return Some(s);
};
}
use EvalAltResult::*;
match ear {
ErrorRuntime(d, _) => {
if d.is_string() {
let s = d.clone().into_string().expect("sting.");
if s == "env(`GITHUB_TOKEN`) not present" {
h!(
r#"gh() requires the environment variable `GITHUB_TOKEN` to be set as a Github token so as to avoid rate-limits:
https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting
To create a Github token, see:
https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"#
);
}
}
}
ErrorMismatchOutputType(req, actual, _) => {
if sl.contains("json(") && req == "map" && actual == "array" {
h!("If the json root is an array `[]`, use json_arr() instead.");
}
}
_ => {}
}
trace!("No hints");
None
}