From 1fb1d3e7b5f4a84810d6212daeb0bda149c56722 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Wed, 5 Jun 2024 20:18:23 +0200 Subject: [PATCH] Improve formatting / printing of userdata and tables with __type and / or __tostring metamethods --- crates/lune-std-regex/src/captures.rs | 2 +- crates/lune-std-regex/src/matches.rs | 2 +- crates/lune-std-regex/src/regex.rs | 2 +- crates/lune-utils/src/fmt/value/basic.rs | 49 ++++++++++++---- .../lune-utils/src/fmt/value/metamethods.rs | 52 ++++++++++------- tests/regex/metamethods.luau | 6 +- tests/stdio/format.luau | 58 +++++++++++++++---- 7 files changed, 121 insertions(+), 50 deletions(-) diff --git a/crates/lune-std-regex/src/captures.rs b/crates/lune-std-regex/src/captures.rs index 5dbea74..fcfde93 100644 --- a/crates/lune-std-regex/src/captures.rs +++ b/crates/lune-std-regex/src/captures.rs @@ -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())) }); } diff --git a/crates/lune-std-regex/src/matches.rs b/crates/lune-std-regex/src/matches.rs index bc109f8..ad21491 100644 --- a/crates/lune-std-regex/src/matches.rs +++ b/crates/lune-std-regex/src/matches.rs @@ -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()) }); } } diff --git a/crates/lune-std-regex/src/regex.rs b/crates/lune-std-regex/src/regex.rs index 9b83544..2ae26d9 100644 --- a/crates/lune-std-regex/src/regex.rs +++ b/crates/lune-std-regex/src/regex.rs @@ -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()) }); } diff --git a/crates/lune-utils/src/fmt/value/basic.rs b/crates/lune-utils/src/fmt/value/basic.rs index cc4f9fb..ff1f421 100644 --- a/crates/lune-utils/src/fmt/value/basic.rs +++ b/crates/lune-utils/src/fmt/value/basic.rs @@ -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("").to_string(), LuaValue::LightUserData(_) => COLOR_MAGENTA.apply_to("").to_string(), LuaValue::UserData(u) => { - if let Some(s) = call_userdata_tostring_metamethod(u) { - s - } else { - COLOR_MAGENTA.apply_to("").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("").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!( + "", + ErrorComponents::from(e.clone()) + )) + .to_string(), + } +} + +fn format_typename_and_tostringed( + fallback: &'static str, + typename: Option, + tostringed: Option, +) -> 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}>"), } } diff --git a/crates/lune-utils/src/fmt/value/metamethods.rs b/crates/lune-utils/src/fmt/value/metamethods.rs index 8b00b1a..c553262 100644 --- a/crates/lune-utils/src/fmt/value/metamethods.rs +++ b/crates/lune-utils/src/fmt/value/metamethods.rs @@ -1,29 +1,37 @@ use mlua::prelude::*; +pub fn get_table_type_metavalue<'a>(tab: &'a LuaTable<'a>) -> Option { + 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 { + let s = tab + .get_metatable() + .ok()? + .get::(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 { - 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 { - let f = match tab.get_metatable() { - Err(_) => None, - Ok(meta) => match meta.get::(LuaMetaMethod::ToString.name()) { - Ok(method) => Some(method), - Err(_) => None, - }, - }?; - match f.call::<_, String>(()) { - Ok(res) => Some(res), - Err(_) => None, - } + tab.get_metatable() + .ok()? + .get::(LuaMetaMethod::ToString.name()) + .ok()? + .call(tab) + .ok() } diff --git a/tests/regex/metamethods.luau b/tests/regex/metamethods.luau index f14231c..2a4f304 100644 --- a/tests/regex/metamethods.luau +++ b/tests/regex/metamethods.luau @@ -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") diff --git a/tests/stdio/format.luau b/tests/stdio/format.luau index 56d673f..7ade5f5 100644 --- a/tests/stdio/format.luau +++ b/tests/stdio/format.luau @@ -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), "" ) -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), - "" + "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 = ,\n Bar = ,\n}" + "{\n Bar = ,\n Baz = ,\n Foo = ,\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), "