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::Len, |_, this, ()| Ok(this.num_captures()));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { 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) { 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::Len, |_, this, ()| Ok(this.range().len()));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { 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, ()| { 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 mlua::prelude::*;
use crate::fmt::ErrorComponents;
use super::{ 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}, 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::Function(_) => COLOR_MAGENTA.apply_to("<function>").to_string(),
LuaValue::LightUserData(_) => COLOR_MAGENTA.apply_to("<pointer>").to_string(), LuaValue::LightUserData(_) => COLOR_MAGENTA.apply_to("<pointer>").to_string(),
LuaValue::UserData(u) => { LuaValue::UserData(u) => {
if let Some(s) = call_userdata_tostring_metamethod(u) { let formatted = format_typename_and_tostringed(
s "userdata",
} else { get_userdata_type_metavalue(u),
COLOR_MAGENTA.apply_to("<userdata>").to_string() call_userdata_tostring_metamethod(u),
} );
COLOR_MAGENTA.apply_to(formatted).to_string()
} }
LuaValue::Table(t) => { LuaValue::Table(t) => {
if let Some(s) = call_table_tostring_metamethod(t) { let formatted = format_typename_and_tostringed(
s "table",
} else { get_table_type_metavalue(t),
COLOR_MAGENTA.apply_to("<table>").to_string() call_table_tostring_metamethod(t),
);
COLOR_MAGENTA.apply_to(formatted).to_string()
} }
} LuaValue::Error(e) => COLOR_MAGENTA
_ => COLOR_MAGENTA.apply_to("<?>").to_string(), .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::*; 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> { pub fn call_table_tostring_metamethod<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
let f = match tab.get_metatable() { tab.get_metatable()?
None => None, .get::<_, LuaFunction>(LuaMetaMethod::ToString.name())
Some(meta) => match meta.get::<_, LuaFunction>(LuaMetaMethod::ToString.name()) { .ok()?
Ok(method) => Some(method), .call(tab)
Err(_) => None, .ok()
},
}?;
match f.call::<_, String>(()) {
Ok(res) => Some(res),
Err(_) => None,
}
} }
pub fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> { pub fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
let f = match tab.get_metatable() { tab.get_metatable()
Err(_) => None, .ok()?
Ok(meta) => match meta.get::<LuaFunction>(LuaMetaMethod::ToString.name()) { .get::<LuaFunction>(LuaMetaMethod::ToString.name())
Ok(method) => Some(method), .ok()?
Err(_) => None, .call(tab)
}, .ok()
}?;
match f.call::<_, String>(()) {
Ok(res) => Some(res),
Err(_) => None,
}
} }

View file

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

View file

@ -1,4 +1,5 @@
local process = require("@lune/process") local process = require("@lune/process")
local regex = require("@lune/regex")
local roblox = require("@lune/roblox") local roblox = require("@lune/roblox")
local stdio = require("@lune/stdio") local stdio = require("@lune/stdio")
@ -9,6 +10,13 @@ local function assertFormatting(errorMessage: string, formatted: string, expecte
end end
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( assertFormatting(
"Should add a single space between arguments", "Should add a single space between arguments",
stdio.format("Hello", "world", "!"), stdio.format("Hello", "world", "!"),
@ -47,25 +55,38 @@ assertFormatting(
local userdatas = { local userdatas = {
Foo = newproxy(false), Foo = newproxy(false),
Bar = (roblox :: any).Vector3.new(), Bar = regex.new("TEST"),
Baz = (roblox :: any).Vector3.new(1, 2, 3),
} }
assertFormatting( assertFormatting(
"Should format userdatas as their type (unknown userdata)", "Should format userdatas as generic 'userdata' if unknown",
stdio.format(userdatas.Foo), stdio.format(userdatas.Foo),
"<userdata>" "<userdata>"
) )
assertFormatting( assertContains(
"Should format userdatas as their type (known userdata)", "Should format userdatas with their type if they have a __type metafield",
stdio.format(userdatas.Bar), 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( assertFormatting(
"Should format userdatas as their type in tables", "Should format userdatas properly in tables",
stdio.format(userdatas), 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 = { local nested = {
@ -80,7 +101,24 @@ local nested = {
}, },
} }
assert( assertContains(
string.find(stdio.format(nested), "Nesting = { ... }", 1, true) ~= nil, "Should print 4 levels of nested tables before cutting off",
"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")