Improve formatting / printing of userdata and tables with __type and / or __tostring metamethods

This commit is contained in:
Filip Tibell 2024-06-05 20:18:23 +02:00
parent 0efc2c565b
commit 1fb1d3e7b5
No known key found for this signature in database
7 changed files with 121 additions and 50 deletions

View file

@ -81,7 +81,7 @@ impl LuaUserData for LuaCaptures {
methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.num_captures()));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("RegexCaptures({})", this.num_captures()))
Ok(format!("{}", this.num_captures()))
});
}

View file

@ -47,7 +47,7 @@ impl LuaUserData for LuaMatch {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.range().len()));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("RegexMatch({})", this.slice()))
Ok(this.slice().to_string())
});
}
}

View file

@ -66,7 +66,7 @@ impl LuaUserData for LuaRegex {
);
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("Regex({})", this.inner.as_str()))
Ok(this.inner.as_str().to_string())
});
}

View file

@ -1,7 +1,12 @@
use mlua::prelude::*;
use crate::fmt::ErrorComponents;
use super::{
metamethods::{call_table_tostring_metamethod, call_userdata_tostring_metamethod},
metamethods::{
call_table_tostring_metamethod, call_userdata_tostring_metamethod,
get_table_type_metavalue, get_userdata_type_metavalue,
},
style::{COLOR_CYAN, COLOR_GREEN, COLOR_MAGENTA, COLOR_YELLOW},
};
@ -56,19 +61,39 @@ pub(crate) fn format_value_styled(value: &LuaValue, prefer_plain: bool) -> Strin
LuaValue::Function(_) => COLOR_MAGENTA.apply_to("<function>").to_string(),
LuaValue::LightUserData(_) => COLOR_MAGENTA.apply_to("<pointer>").to_string(),
LuaValue::UserData(u) => {
if let Some(s) = call_userdata_tostring_metamethod(u) {
s
} else {
COLOR_MAGENTA.apply_to("<userdata>").to_string()
}
let formatted = format_typename_and_tostringed(
"userdata",
get_userdata_type_metavalue(u),
call_userdata_tostring_metamethod(u),
);
COLOR_MAGENTA.apply_to(formatted).to_string()
}
LuaValue::Table(t) => {
if let Some(s) = call_table_tostring_metamethod(t) {
s
} else {
COLOR_MAGENTA.apply_to("<table>").to_string()
}
let formatted = format_typename_and_tostringed(
"table",
get_table_type_metavalue(t),
call_table_tostring_metamethod(t),
);
COLOR_MAGENTA.apply_to(formatted).to_string()
}
_ => COLOR_MAGENTA.apply_to("<?>").to_string(),
LuaValue::Error(e) => COLOR_MAGENTA
.apply_to(format!(
"<LuaError(\n{})>",
ErrorComponents::from(e.clone())
))
.to_string(),
}
}
fn format_typename_and_tostringed(
fallback: &'static str,
typename: Option<String>,
tostringed: Option<String>,
) -> String {
match (typename, tostringed) {
(Some(typename), Some(tostringed)) => format!("<{typename}({tostringed})>"),
(Some(typename), None) => format!("<{typename}>"),
(None, Some(tostringed)) => format!("<{tostringed}>"),
(None, None) => format!("<{fallback}>"),
}
}

View file

@ -1,29 +1,37 @@
use mlua::prelude::*;
pub fn get_table_type_metavalue<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
let s = tab
.get_metatable()?
.get::<_, LuaString>(LuaMetaMethod::Type.name())
.ok()?;
let s = s.to_str().ok()?;
Some(s.to_string())
}
pub fn get_userdata_type_metavalue<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
let s = tab
.get_metatable()
.ok()?
.get::<LuaString>(LuaMetaMethod::Type.name())
.ok()?;
let s = s.to_str().ok()?;
Some(s.to_string())
}
pub fn call_table_tostring_metamethod<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
let f = match tab.get_metatable() {
None => None,
Some(meta) => match meta.get::<_, LuaFunction>(LuaMetaMethod::ToString.name()) {
Ok(method) => Some(method),
Err(_) => None,
},
}?;
match f.call::<_, String>(()) {
Ok(res) => Some(res),
Err(_) => None,
}
tab.get_metatable()?
.get::<_, LuaFunction>(LuaMetaMethod::ToString.name())
.ok()?
.call(tab)
.ok()
}
pub fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
let f = match tab.get_metatable() {
Err(_) => None,
Ok(meta) => match meta.get::<LuaFunction>(LuaMetaMethod::ToString.name()) {
Ok(method) => Some(method),
Err(_) => None,
},
}?;
match f.call::<_, String>(()) {
Ok(res) => Some(res),
Err(_) => None,
}
tab.get_metatable()
.ok()?
.get::<LuaFunction>(LuaMetaMethod::ToString.name())
.ok()?
.call(tab)
.ok()
}

View file

@ -3,14 +3,14 @@
local regex = require("@lune/regex")
local re = regex.new("[0-9]+")
assert(tostring(re) == "Regex([0-9]+)")
assert(tostring(re) == "[0-9]+")
assert(typeof(re) == "Regex")
local mtch = re:find("1337 wow")
assert(tostring(mtch) == "RegexMatch(1337)")
assert(tostring(mtch) == "1337")
assert(typeof(mtch) == "RegexMatch")
local re2 = regex.new("([0-9]+) ([0-9]+) wow! ([0-9]+) ([0-9]+)")
local captures = re2:captures("1337 125600 wow! 1984 0")
assert(tostring(captures) == "RegexCaptures(4)")
assert(tostring(captures) == "4")
assert(typeof(captures) == "RegexCaptures")

View file

@ -1,4 +1,5 @@
local process = require("@lune/process")
local regex = require("@lune/regex")
local roblox = require("@lune/roblox")
local stdio = require("@lune/stdio")
@ -9,6 +10,13 @@ local function assertFormatting(errorMessage: string, formatted: string, expecte
end
end
local function assertContains(errorMessage: string, haystack: string, needle: string)
if string.find(haystack, needle) == nil then
stdio.ewrite(string.format("%s\nHaystack: %s\nNeedle: %s", errorMessage, needle, haystack))
process.exit(1)
end
end
assertFormatting(
"Should add a single space between arguments",
stdio.format("Hello", "world", "!"),
@ -47,25 +55,38 @@ assertFormatting(
local userdatas = {
Foo = newproxy(false),
Bar = (roblox :: any).Vector3.new(),
Bar = regex.new("TEST"),
Baz = (roblox :: any).Vector3.new(1, 2, 3),
}
assertFormatting(
"Should format userdatas as their type (unknown userdata)",
"Should format userdatas as generic 'userdata' if unknown",
stdio.format(userdatas.Foo),
"<userdata>"
)
assertFormatting(
"Should format userdatas as their type (known userdata)",
assertContains(
"Should format userdatas with their type if they have a __type metafield",
stdio.format(userdatas.Bar),
"<Vector3>"
"Regex"
)
assertContains(
"Should format userdatas with their type even if they have a __tostring metamethod",
stdio.format(userdatas.Baz),
"Vector3"
)
assertContains(
"Should format userdatas with their tostringed value if they have a __tostring metamethod",
stdio.format(userdatas.Baz),
"1, 2, 3"
)
assertFormatting(
"Should format userdatas as their type in tables",
"Should format userdatas properly in tables",
stdio.format(userdatas),
"{\n Foo = <userdata>,\n Bar = <Vector3>,\n}"
"{\n Bar = <Regex(TEST)>,\n Baz = <Vector3(1, 2, 3)>,\n Foo = <userdata>,\n}"
)
local nested = {
@ -80,7 +101,24 @@ local nested = {
},
}
assert(
string.find(stdio.format(nested), "Nesting = { ... }", 1, true) ~= nil,
"Should print 4 levels of nested tables before cutting off"
assertContains(
"Should print 4 levels of nested tables before cutting off",
stdio.format(nested),
"Nesting = { ... }"
)
local _, errorMessage = pcall(function()
local function innerInnerFn()
process.spawn("PROGRAM_THAT_DOES_NOT_EXIST")
end
local function innerFn()
innerInnerFn()
end
innerFn()
end)
stdio.ewrite(typeof(errorMessage))
assertContains("Should format errors similarly to userdata", stdio.format(errorMessage), "<LuaErr")
assertContains("Should format errors with stack begins", stdio.format(errorMessage), "Stack Begin")
assertContains("Should format errors with stack ends", stdio.format(errorMessage), "Stack End")