From 7f5e7732258feb480780be0392f2268b6185d639 Mon Sep 17 00:00:00 2001 From: seele Date: Tue, 17 Mar 2026 22:36:13 +0800 Subject: [PATCH 1/9] refactor: simplify error handling and rename TypeError to TypeException --- src/catter/core/capi/type.h | 2 +- src/catter/core/qjs.h | 225 ++++++++++++++++++++++++------------ 2 files changed, 152 insertions(+), 75 deletions(-) diff --git a/src/catter/core/capi/type.h b/src/catter/core/capi/type.h index 400d8f1..789e874 100644 --- a/src/catter/core/capi/type.h +++ b/src/catter/core/capi/type.h @@ -127,7 +127,7 @@ T read_property(const qjs::Object& object, std::string_view property_name) { if constexpr(is_optional_v) { using value_type = typename is_optional::value_type; auto optional_value = object.get_optional_property(std::string(property_name)); - if(!optional_value.has_value() || optional_value->is_nothing()) { + if(!optional_value.has_value()) { return std::nullopt; } return from_property_value(*optional_value); diff --git a/src/catter/core/qjs.h b/src/catter/core/qjs.h index e56a750..88f1df5 100644 --- a/src/catter/core/qjs.h +++ b/src/catter/core/qjs.h @@ -59,33 +59,6 @@ struct value_trans; template struct object_trans; - -inline std::string dump(JSContext* ctx) { - JSValue exception_val = JS_GetException(ctx); - - // Get the error name - JSValue name = JS_GetPropertyStr(ctx, exception_val, "name"); - const char* error_name = JS_ToCString(ctx, name); - - // Get the stack trace - JSValue stack = JS_GetPropertyStr(ctx, exception_val, "stack"); - const char* stack_str = JS_ToCString(ctx, stack); - - // Get the message - const char* msg = JS_ToCString(ctx, exception_val); - std::string result = std::format("Error Name: {}\nMessage: {}\nStack Trace:\n{}", - error_name ? error_name : "Unknown", - msg ? msg : "No message", - stack_str ? stack_str : "No stack trace"); - - JS_FreeCString(ctx, error_name); - JS_FreeCString(ctx, stack_str); - JS_FreeCString(ctx, msg); - JS_FreeValue(ctx, name); - JS_FreeValue(ctx, stack); - JS_FreeValue(ctx, exception_val); - return result; -} } // namespace detail /** @@ -106,9 +79,17 @@ class Exception : public std::exception { std::string details; }; -class TypeError : public Exception { +class TypeException : public Exception { +public: + TypeException(const std::string& details) : Exception(std::format("TypeError: {}", details)) {} +}; + +class Error; + +class JSException : public Exception { public: - TypeError(const std::string& details) : Exception(std::format("TypeError: {}", details)) {} + inline JSException(const Error& error); + inline static JSException dump(JSContext* ctx); }; /** @@ -319,7 +300,7 @@ class Object : protected Value { auto ret = Value{this->context(), JS_GetPropertyStr(this->context(), this->value(), prop_name.c_str())}; if(ret.is_exception()) { - throw qjs::Exception(detail::dump(this->context())); + throw qjs::JSException::dump(this->context()); } return ret; } @@ -341,15 +322,15 @@ class Object : protected Value { * @return std::optional */ std::optional get_optional_property(const std::string& prop_name) const noexcept { - auto ret = Value{this->context(), - JS_GetPropertyStr(this->context(), this->value(), prop_name.c_str())}; - if(ret.is_exception()) { - detail::dump(this->context()); - return std::nullopt; - } else if(ret.is_undefined()) { + try { + if(auto ret = get_property(prop_name); ret.is_undefined()) { + return std::nullopt; + } else { + return ret; + } + } catch(const qjs::Exception&) { return std::nullopt; } - return ret; } /** @@ -367,7 +348,7 @@ class Object : protected Value { JSValue js_val = JS_DupValue(this->context(), val); int ret = JS_SetPropertyStr(this->context(), this->value(), prop_name.c_str(), js_val); if(ret < 0) { - throw qjs::Exception(detail::dump(this->context())); + throw qjs::JSException::dump(this->context()); } } else if constexpr(requires { { val.value() } -> std::convertible_to; @@ -375,7 +356,7 @@ class Object : protected Value { JSValue js_val = JS_DupValue(this->context(), val.value()); int ret = JS_SetPropertyStr(this->context(), this->value(), prop_name.c_str(), js_val); if(ret < 0) { - throw qjs::Exception(detail::dump(this->context())); + throw qjs::JSException::dump(this->context()); } } else { auto js_val = Value::from>(this->context(), std::forward(val)); @@ -384,7 +365,7 @@ class Object : protected Value { prop_name.c_str(), js_val.release()); if(ret < 0) { - throw qjs::Exception(detail::dump(this->context())); + throw qjs::JSException::dump(this->context()); } } } @@ -440,6 +421,51 @@ class Object : protected Value { } }; +class Error : protected Object { +public: + using Object::Object; + using Object::is_valid; + using Object::value; + using Object::context; + using Object::operator bool; + using Object::release; + + Error(JSContext* ctx, const JSValue& val) : Object(ctx, val) {} + + Error(JSContext* ctx, JSValue&& val) : Object(ctx, std::move(val)) {} + + Error(const Error&) = default; + Error(Error&& other) = default; + Error& operator= (const Error&) = default; + Error& operator= (Error&& other) = default; + ~Error() = default; + + std::string message() const { + return this->get_property("message").as(); + } + + std::string stack() const { + return this->get_property("stack").as(); + } + + std::string name() const { + return this->get_property("name").as(); + } + + std::string to_string() const { + return std::format("{}: {}\nStack Trace:\n{}", + this->name(), + this->message(), + this->stack()); + } +}; + +inline JSException::JSException(const Error& error) : Exception(error.to_string()) {} + +inline JSException JSException::dump(JSContext* ctx) { + return JSException(Error(ctx, JS_GetException(ctx))); +} + /** * @brief A typed wrapper for JavaScript functions. * This class allows calling JavaScript functions from C++ and creating C++ callbacks that can be @@ -592,7 +618,7 @@ class Function : protected Object { } if(value.is_exception()) { - throw qjs::Exception(detail::dump(this->context())); + throw qjs::JSException::dump(this->context()); } if constexpr(std::is_void_v) { @@ -788,7 +814,7 @@ class Function : protected Object { for(const auto& arg: args) { if(!arg.is_valid()) { - throw TypeError("Function argument contains an invalid value"); + throw TypeException("Function argument contains an invalid value"); } argv.push_back(JS_DupValue(this->context(), arg.value())); } @@ -801,7 +827,7 @@ class Function : protected Object { } if(value.is_exception()) { - throw qjs::Exception(detail::dump(this->context())); + throw qjs::JSException::dump(this->context()); } if constexpr(std::is_void_v) { @@ -902,7 +928,7 @@ class Array : protected Object { uint32_t length() const { qjs::Value len_val = this->get_property("length"); if(len_val.is_exception()) { - throw qjs::Exception(detail::dump(this->context())); + throw qjs::JSException::dump(this->context()); } return len_val.as(); } @@ -911,7 +937,7 @@ class Array : protected Object { auto val = catter::qjs::Value{this->context(), JS_GetPropertyUint32(this->context(), this->value(), index)}; if(val.is_exception()) { - throw qjs::TypeError(detail::dump(this->context())); + throw qjs::JSException::dump(this->context()); } return val.as(); } @@ -919,7 +945,7 @@ class Array : protected Object { std::optional get(uint32_t index) const noexcept { try { return this->operator[] (index); - } catch(const qjs::TypeError&) { + } catch(const qjs::TypeException&) { return std::nullopt; } } @@ -930,7 +956,7 @@ class Array : protected Object { uint32_t len = this->length(); auto res = JS_SetPropertyUint32(this->context(), this->value(), len, js_val.release()); if(res < 0) { - throw qjs::TypeError(detail::dump(this->context())); + throw qjs::JSException::dump(this->context()); } } @@ -970,7 +996,7 @@ class Array : protected Object { result.push_back(arr[i]); } return result; - } catch(const qjs::TypeError&) { + } catch(const qjs::TypeException&) { return std::nullopt; } } @@ -1000,7 +1026,7 @@ struct value_trans { static bool as(const Value& val) { if(!JS_IsBool(val.value())) { - throw TypeError("Value is not a boolean"); + throw TypeException("Value is not a boolean"); } return JS_ToBool(val.context(), val.value()); } @@ -1008,7 +1034,7 @@ struct value_trans { static std::optional to(const Value& val) noexcept { try { return as(val); - } catch(const TypeError&) { + } catch(const TypeException&) { return std::nullopt; } } @@ -1029,19 +1055,19 @@ struct value_trans { static Num as(const Value& val) { if(!JS_IsNumber(val.value())) { - throw TypeError("Value is not a number"); + throw TypeException("Value is not a number"); } if constexpr(std::is_unsigned_v) { if constexpr(sizeof(Num) <= sizeof(uint32_t)) { uint32_t temp; if(JS_ToUint32(val.context(), &temp, val.value()) < 0) { - throw TypeError("Failed to convert value to uint32_t"); + throw TypeException("Failed to convert value to uint32_t"); } return static_cast(temp); } else { uint64_t temp; if(JS_ToIndex(val.context(), &temp, val.value()) < 0) { - throw TypeError("Failed to convert value to uint32_t"); + throw TypeException("Failed to convert value to uint32_t"); } return static_cast(temp); } @@ -1049,13 +1075,13 @@ struct value_trans { if constexpr(sizeof(Num) <= sizeof(int32_t)) { int32_t temp; if(JS_ToInt32(val.context(), &temp, val.value()) < 0) { - throw TypeError("Failed to convert value to uint32_t"); + throw TypeException("Failed to convert value to uint32_t"); } return static_cast(temp); } else { int64_t temp; if(JS_ToInt64(val.context(), &temp, val.value()) < 0) { - throw TypeError("Failed to convert value to int64_t"); + throw TypeException("Failed to convert value to int64_t"); } return static_cast(temp); } @@ -1068,7 +1094,7 @@ struct value_trans { static std::optional to(const Value& val) noexcept { try { return as(val); - } catch(const TypeError&) { + } catch(const TypeException&) { return std::nullopt; } } @@ -1082,12 +1108,12 @@ struct value_trans { static std::string as(const Value& val) { if(!JS_IsString(val.value())) { - throw TypeError("Value is not a string"); + throw TypeException("Value is not a string"); } size_t len; const char* str = JS_ToCStringLen(val.context(), &len, val.value()); if(str == nullptr) { - throw TypeError("Failed to convert value to string"); + throw TypeException("Failed to convert value to string"); } std::string result{str, len}; JS_FreeCString(val.context(), str); @@ -1097,7 +1123,7 @@ struct value_trans { static std::optional to(const Value& val) noexcept { try { return as(val); - } catch(const TypeError&) { + } catch(const TypeException&) { return std::nullopt; } } @@ -1116,7 +1142,7 @@ struct value_trans { static Object as(const Value& val) { if(!JS_IsObject(val.value())) { - throw TypeError("Value is not an object"); + throw TypeException("Value is not an object"); } return Object{val.context(), val.value()}; } @@ -1124,7 +1150,31 @@ struct value_trans { static std::optional to(const Value& val) noexcept { try { return as(val); - } catch(const TypeError&) { + } catch(const TypeException&) { + return std::nullopt; + } + } +}; + +template <> +struct value_trans { + static Value from(const Error& value) noexcept { + return Value{value.context(), value.value()}; + } + + static Value from(Error&& value) noexcept { + auto ctx = value.context(); + return Value{ctx, value.release()}; + } + + static Error as(const Value& val) { + return val.as().as(); + } + + static std::optional to(const Value& val) noexcept { + try { + return as(val); + } catch(const TypeException&) { return std::nullopt; } } @@ -1148,7 +1198,7 @@ struct value_trans> { static std::optional> to(const Value& val) noexcept { try { return as(val); - } catch(const TypeError&) { + } catch(const TypeException&) { return std::nullopt; } } @@ -1174,7 +1224,34 @@ struct value_trans> { static std::optional to(const Value& val) noexcept { try { return as(val); - } catch(const TypeError&) { + } catch(const TypeException&) { + return std::nullopt; + } + } +}; + +template <> +struct object_trans { + static Object from(const Error& value) noexcept { + return Object{value.context(), value.value()}; + } + + static Object from(Error&& value) noexcept { + auto ctx = value.context(); + return Object{ctx, value.release()}; + } + + static Error as(const Object& obj) { + if(!JS_IsError(obj.value())) { + throw TypeException("Object is not an error"); + } + return Error{obj.context(), obj.value()}; + } + + static std::optional to(const Object& obj) noexcept { + try { + return as(obj); + } catch(const TypeException&) { return std::nullopt; } } @@ -1195,7 +1272,7 @@ struct object_trans> { static ArrTy as(const Object& obj) { if(!JS_IsArray(obj.value())) { - throw TypeError("Object is not an array"); + throw TypeException("Object is not an array"); } return ArrTy{obj.context(), obj.value()}; } @@ -1203,7 +1280,7 @@ struct object_trans> { static std::optional to(const Object& obj) noexcept { try { return as(obj); - } catch(const TypeError&) { + } catch(const TypeException&) { return std::nullopt; } } @@ -1224,11 +1301,11 @@ struct object_trans> { static FuncType as(const Object& obj) { if(!JS_IsFunction(obj.context(), obj.value())) { - throw TypeError("Object is not a function"); + throw TypeException("Object is not a function"); } if(obj.get_property("length").as() != sizeof...(Args)) { - throw TypeError("Function has incorrect number of arguments"); + throw TypeException("Function has incorrect number of arguments"); } return FuncType{obj.context(), obj.value()}; @@ -1237,7 +1314,7 @@ struct object_trans> { static std::optional to(const Object& val) noexcept { try { return as(val); - } catch(const TypeError&) { + } catch(const TypeException&) { return std::nullopt; } } @@ -1258,7 +1335,7 @@ struct object_trans> { static FuncType as(const Object& obj) { if(!JS_IsFunction(obj.context(), obj.value())) { - throw TypeError("Object is not a function"); + throw TypeException("Object is not a function"); } return FuncType{obj.context(), obj.value()}; @@ -1267,7 +1344,7 @@ struct object_trans> { static std::optional to(const Object& val) noexcept { try { return as(val); - } catch(const TypeError&) { + } catch(const TypeException&) { return std::nullopt; } } @@ -1395,7 +1472,7 @@ class Context { if(this->has_exception()) { JS_FreeValue(this->js_context(), val); - throw qjs::Exception(detail::dump(this->js_context())); + throw qjs::JSException::dump(this->js_context()); } return Value{this->js_context(), std::move(val)}; } @@ -1546,7 +1623,7 @@ std::string stringify(T&& v) { auto val = v.value(); auto json_str_val = qjs::Value{ctx, JS_JSONStringify(ctx, val, JS_UNDEFINED, JS_UNDEFINED)}; if(json_str_val.is_exception()) { - throw qjs::Exception(detail::dump(ctx)); + throw qjs::JSException::dump(ctx); } const char* json_cstr = JS_ToCString(ctx, json_str_val.value()); @@ -1555,7 +1632,7 @@ std::string stringify(T&& v) { JS_FreeCString(ctx, json_cstr); return result; } - throw qjs::Exception("Failed to stringify value"); + throw qjs::TypeException("Failed to convert value to JSON string"); }; // namespace json inline qjs::Value parse(const std::string& json_str, const Context& ctx) { @@ -1565,7 +1642,7 @@ inline qjs::Value parse(const std::string& json_str, const Context& ctx) { JS_ParseJSON(ctx.js_context(), json_str.data(), json_str.size(), "")}; if(ret.is_exception()) { - throw qjs::Exception(detail::dump(ctx.js_context())); + throw qjs::JSException::dump(ctx.js_context()); } return ret; } From 9a1c628eb8f10d9b80b8cfaa85d1a7ccfd639fb0 Mon Sep 17 00:00:00 2001 From: seele Date: Tue, 17 Mar 2026 22:46:01 +0800 Subject: [PATCH 2/9] fix: enhance exception handling by using qjs::Exception for error reporting --- src/catter/core/qjs.h | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/catter/core/qjs.h b/src/catter/core/qjs.h index 88f1df5..19f0e0b 100644 --- a/src/catter/core/qjs.h +++ b/src/catter/core/qjs.h @@ -68,8 +68,13 @@ struct object_trans; */ class Exception : public std::exception { public: - Exception(const std::string& details) : - details(std::format("{}\n{}", details, cpptrace::generate_trace().to_string())) {} + Exception(const std::string& details) : details(details) {} + + Exception(std::string&& details) : details(std::move(details)) {} + + template + Exception(std::format_string fmt, Args&&... args) : + Exception(std::format(fmt, std::forward(args)...)) {} const char* what() const noexcept override { return details.c_str(); @@ -1375,8 +1380,7 @@ class CModule { Value{this->ctx, func.value()} }); if(JS_AddModuleExport(this->ctx, m, name.c_str()) < 0) { - throw std::runtime_error( - std::format("Failed to add export '{}' to module '{}'", name, this->name)); + throw qjs::Exception("Failed to add export '{}' to module '{}'", name, this->name); } return *this; } @@ -1387,8 +1391,7 @@ class CModule { Value{this->ctx, JS_NewCFunction(this->ctx, func, name.c_str(), argc)} }); if(JS_AddModuleExport(this->ctx, m, name.c_str()) < 0) { - throw std::runtime_error( - std::format("Failed to add export '{}' to module '{}'", name, this->name)); + throw qjs::Exception("Failed to add export '{}' to module '{}'", name, this->name); } return *this; } @@ -1459,7 +1462,7 @@ class Context { return 0; }); if(m == nullptr) { - throw std::runtime_error("Failed to create new C module"); + throw qjs::Exception("Failed to create new C module"); } return this->raw->modules.emplace(name, CModule(this->js_context(), m, name)) @@ -1554,7 +1557,7 @@ class Runtime { static Runtime create() { auto js_rt = JS_NewRuntime(); if(!js_rt) { - throw std::runtime_error("Failed to create new JS runtime"); + throw qjs::Exception("Failed to create new JS runtime"); } return Runtime(js_rt); } @@ -1567,7 +1570,7 @@ class Runtime { } else { auto js_ctx = JS_NewContext(this->js_runtime()); if(!js_ctx) { - throw std::runtime_error("Failed to create new JS context"); + throw qjs::Exception("Failed to create new JS context"); } return this->raw->ctxs.emplace(name, Context(js_ctx)).first->second; } From 5f0a749bd6ea3c5aead2c7c7ae75f741ba9d8e53 Mon Sep 17 00:00:00 2001 From: seele Date: Tue, 17 Mar 2026 23:00:18 +0800 Subject: [PATCH 3/9] test: add case for error handling in JSON stringify and variadic arguments --- tests/unit/catter/core/qjs.cc | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/unit/catter/core/qjs.cc b/tests/unit/catter/core/qjs.cc index bf2a735..a61b045 100644 --- a/tests/unit/catter/core/qjs.cc +++ b/tests/unit/catter/core/qjs.cc @@ -1,3 +1,4 @@ +#include #include #include #include @@ -493,6 +494,41 @@ TEST_SUITE(qjs_tests) { "Value is not a number")); }; + TEST_CASE(error_and_json_helpers_cover_metadata_stringify_and_invalid_variadic_args) { + auto f = [&]() { + auto runtime = qjs::Runtime::create(); + auto& ctx = runtime.context(); + + auto error = ctx.eval("new TypeError('boom')", "", eval_flags) + .as() + .as(); + + EXPECT_TRUE(error.name() == "TypeError"); + EXPECT_TRUE(error.message() == "boom"); + EXPECT_TRUE(error.stack().contains(":1:4")); + EXPECT_TRUE(error.to_string().contains("Stack Trace:")); + + auto parsed = qjs::json::parse(R"({"number":1,"text":"ok"})", ctx).as(); + auto dumped = qjs::json::stringify(parsed); + EXPECT_TRUE(dumped.contains(R"("number":1)")); + EXPECT_TRUE(dumped.contains(R"("text":"ok")")); + + auto cyclic = + ctx.eval("const x = {}; x.self = x; x;", "", eval_flags).as(); + EXPECT_TRUE( + throws_with_message([&]() { (void)qjs::json::stringify(cyclic); }, "TypeError")); + + auto count_args = qjs::Function::from( + ctx.js_context(), + [](qjs::Parameters args) { return static_cast(args.size()); }); + qjs::Parameters invalid_args{}; + invalid_args.emplace_back(); + EXPECT_TRUE( + throws_with_message([&]() { (void)count_args(invalid_args); }, "invalid value")); + }; + EXPECT_NOTHROWS(f()); + }; + TEST_CASE(object_register_reuses_class_id_per_runtime_and_separates_runtimes) { auto runtime_a = qjs::Runtime::create(); auto& ctx_a = runtime_a.context(); From 091a3cf36bcba484eccdb13c3f8e3ac611cf406b Mon Sep 17 00:00:00 2001 From: seele Date: Wed, 18 Mar 2026 20:59:22 +0800 Subject: [PATCH 4/9] refactor: update logging format and improve error message checks in js tests --- api/src/util/cmd.ts | 2 +- src/catter/core/apitool.h | 6 +-- src/catter/core/js.cc | 71 +++++------------------------------- tests/unit/catter/core/js.cc | 6 +-- 4 files changed, 17 insertions(+), 68 deletions(-) diff --git a/api/src/util/cmd.ts b/api/src/util/cmd.ts index cd530e0..c2569dd 100644 --- a/api/src/util/cmd.ts +++ b/api/src/util/cmd.ts @@ -1,4 +1,4 @@ -import { option } from "../index.js"; +import * as option from "../option/index.js"; import { ClangID } from "../option/clang.js"; /** diff --git a/src/catter/core/apitool.h b/src/catter/core/apitool.h index de3fa22..85fa287 100644 --- a/src/catter/core/apitool.h +++ b/src/catter/core/apitool.h @@ -80,20 +80,20 @@ static R invoke_with_log(const std::string& args_s, CallArgs&&... call_args) { try { if constexpr(std::is_void_v) { V(std::forward(call_args)...); - LOG_INFO("Invoke C API [{}]:\n -> args = {}\n -> ret = ", + LOG_INFO("Invoke C API `{}`:\n -> args = {}\n -> ret = ", capi_name(), args_s); return; } else { auto ret = V(std::forward(call_args)...); - LOG_INFO("Invoke C API [{}]:\n -> args = {}\n -> ret = {}", + LOG_INFO("Invoke C API `{}`:\n -> args = {}\n -> ret = {}", capi_name(), args_s, serialize_value(ret)); return ret; } } catch(const std::exception& e) { - LOG_INFO("Invoke C API [{}]:\n -> args = {}\n -> throw = {}", + LOG_INFO("Invoke C API `{}`:\n -> args = {}\n -> throw = {}", capi_name(), args_s, e.what()); diff --git a/src/catter/core/js.cc b/src/catter/core/js.cc index 04345dd..88f9504 100644 --- a/src/catter/core/js.cc +++ b/src/catter/core/js.cc @@ -33,65 +33,6 @@ struct Self { namespace { Self self{}; - -std::optional property_to_string(JSContext* ctx, JSValueConst value, const char* key) { - JSValue prop = JS_GetPropertyStr(ctx, value, key); - if(JS_IsException(prop)) { - JS_FreeValue(ctx, JS_GetException(ctx)); - return std::nullopt; - } - - std::optional result = std::nullopt; - if(!JS_IsUndefined(prop) && !JS_IsNull(prop)) { - if(const char* str = JS_ToCString(ctx, prop)) { - result = str; - JS_FreeCString(ctx, str); - } else if(JS_HasException(ctx)) { - JS_FreeValue(ctx, JS_GetException(ctx)); - } - } - - JS_FreeValue(ctx, prop); - return result; -} - -std::string stringify_value(JSContext* ctx, JSValueConst value) { - if(const char* str = JS_ToCString(ctx, value)) { - std::string result = str; - JS_FreeCString(ctx, str); - return result; - } - - if(JS_HasException(ctx)) { - JS_FreeValue(ctx, JS_GetException(ctx)); - } - return ""; -} - -std::string format_rejection_reason(const qjs::Value& value) { - auto* ctx = value.context(); - auto message = property_to_string(ctx, value.value(), "message"); - auto stack = property_to_string(ctx, value.value(), "stack"); - - if(message.has_value() || stack.has_value()) { - std::string result; - if(message.has_value()) { - result += std::format("Error Message: {}\n", *message); - } - if(stack.has_value()) { - result += std::format("Stack Trace:\n{}\n", *stack); - } - return result; - } - - return std::format("{}\n", stringify_value(ctx, value.value())); -} - -void append_rejection_trace(std::string& error_trace, const qjs::Parameters& args) { - for(const auto& arg: args) { - error_trace += format_rejection_reason(arg); - } -} } // namespace CatterConfig on_start(CatterConfig config) { @@ -164,7 +105,9 @@ void sync_eval(std::string_view input, const char* filename, int eval_flags) { auto reject = CallBack::from(js_ctx, [&](qjs::Parameters args) { state = Rejected; - append_rejection_trace(error_strace, args); + for(auto& arg: args) { + error_strace += arg.as().to_string() + "\n"; + } }); auto then_promise = promise_obj["then"].as().invoke(promise_obj, @@ -173,7 +116,13 @@ void sync_eval(std::string_view input, const char* filename, int eval_flags) { auto catch_fn = CallBack::from(js_ctx, [&](qjs::Parameters args) { state = Rejected; - append_rejection_trace(error_strace, args); + try { + for(auto& arg: args) { + error_strace += arg.as().to_string() + "\n"; + } + } catch(const std::exception& e) { + error_strace += std::format("Exception: {}\n", e.what()); + } }); then_promise["catch"].as().invoke(then_promise, qjs::Object::from(catch_fn)); diff --git a/tests/unit/catter/core/js.cc b/tests/unit/catter/core/js.cc index d11b0d1..bf3605b 100644 --- a/tests/unit/catter/core/js.cc +++ b/tests/unit/catter/core/js.cc @@ -197,9 +197,9 @@ TEST_SUITE(js_tests) { } catch(const catter::qjs::Exception& ex) { caught = true; std::string message = ex.what(); - EXPECT_TRUE(message.find("Error Message: async boom") != std::string::npos); - EXPECT_TRUE(message.find("Stack Trace:") != std::string::npos); - EXPECT_TRUE(message.find("reject.js") != std::string::npos); + EXPECT_TRUE(message.contains("async boom")); + EXPECT_TRUE(message.contains("Stack Trace:")); + EXPECT_TRUE(message.contains("reject.js")); } EXPECT_TRUE(caught); From 4cd99d3fefbb7a2f1c1220d75050cb83ab8578a3 Mon Sep 17 00:00:00 2001 From: seele Date: Wed, 18 Mar 2026 21:00:33 +0800 Subject: [PATCH 5/9] chore: remove outdated internal and plugin API documentation from api.md --- doc/api.md | 159 +---------------------------------------------------- 1 file changed, 1 insertion(+), 158 deletions(-) diff --git a/doc/api.md b/doc/api.md index ee50c38..30404ce 100644 --- a/doc/api.md +++ b/doc/api.md @@ -1,158 +1 @@ -```typescript -// ------------------------------------------------------------ -// Internal API, not for plugin developers -// ------------------------------------------------------------ -const ActionKind = ['skip', 'drop', 'abort', 'modify'] as const; - -/** - * @type skip - skip this command, but execute the original command - * @type drop - drop this command, do not execute the original command - * @type abort - abort the whole execution, and return an error - * @type modify - modify this command, and execute the modified command - */ -type ActionType = (typeof ActionKind)[number]; - -type Action = { - // for modify - data?: CommandData; - type: ActionType; -}; - -const EventKind = ['finish', 'output'] as const; -type EventType = (typeof EventKind)[number]; - -type ExecutionEvent = { - // for output - stdout?: string; - stderr?: string; - code: number; - type: EventType; -}; - -type CatterRuntime = { - supportActions: ActionType[]; - supportEvents: EventType[]; - // eslogger: only in mac - // env: eg. CC=catter-proxy, then proxy report this cmd - type: 'inject' | 'eslogger' | 'env'; - supportParentId: boolean; -}; - -/** - * @field scriptArgs - the arguments of this script - * @field scriptPath - the path of this script - * @field buildSystemCommand - the command to execute this script in build system, eg. ['bazel', 'build', '//:target'] - * @field isScriptSupported - defaults to true, if false, catter will instantly abort the execution and return an error. - * @field runtime - the runtime environment of this script, can be used to determine which actions and events are supported - * @field options - the options of catter, can be used to enable some features of catter, eg. log - */ -type CatterConfig = { - scriptPath: string; - scriptArgs: string[]; - buildSystemCommand: string[]; - runtime: CatterRuntime; - options: { - log: boolean; - }; - isScriptSupported: boolean; -}; - -type CatterErr = { - //... -}; - -/** - * @field parent - When supportParentId is true at runtime, this field is the ID of the parent command that generated this command; otherwise, this field is undefined. - * @field env - the environment variables of this command, in the format of ["KEY=VALUE", ...] - */ -type CommandData = { - cwd: string; - exe: string; - argv: string[]; - env: string[]; - runtime: CatterRuntime; - parent?: number; -}; - -export function service_on_start(cb: (config: CatterConfig) => CatterConfig): void; -export function service_on_finish(cb: () => void): void; -export function service_on_command(cb: (id: number, data: CommandData | CatterErr) => Action): void; -export function service_on_execution(cb: (id: number, event: ExecutionEvent) => void): void; - - -// ------------------------------------------------------------ -// Plugin API, for plugin developers -// ------------------------------------------------------------ - -import { service_on_start, service_on_finish, service_on_command, service_on_execution } from 'catter-c'; - -/** - * @method onStart - called when catter start, can modify config - * @method onFinish - called when catter finish - * @method onCommand - called when a command being captured - * @param onCommand.id - a unique identifier for this command, can be used to correlate with onExecution - * @param onCommand.data - the data of this command, if there is an error during capturing, this will be a CatterErr object - * @method onExecution - called when a command being executed, can listen on its output and finish event. - * @param onExecution.id - the unique identifier for this command, same as onCommand - * @param onExecution.event - the event of this command, if there is an error during execution, the code field will be non-zero and stdout/stderr may be undefined - */ -interface CatterService { - onStart: (config: CatterConfig) => CatterConfig; - onFinish: () => void; - onCommand: (id: number, data: CommandData | CatterErr) => Action; - onExecution: (id: number, event: ExecutionEvent) => void; -} - -export function onStart(cb: (config: CatterConfig) => CatterConfig): void{ - service_on_start(cb); -} -export function onFinish(cb: () => void): void { - service_on_finish(cb); -} -export function onCommand(cb: (id: number, data: CommandData | CatterErr) => Action): void { - service_on_command(cb); -} -export function onExecution(cb: (id: number, event: ExecutionEvent) => void): void { - service_on_execution(cb); -} - - -export function register(service: CatterService) { - onStart(service.onStart); - onFinish(service.onFinish); - onCommand(service.onCommand); - onExecution(service.onExecution); -} - -// ------------------------------------------------------------ -// Example -// ------------------------------------------------------------ -class MyCatterPlugin implements CatterService { - dataMap: Map = new Map(); - eventMap: Map = new Map(); - onStart(config: CatterConfig): CatterConfig { - // modify config - return config; - } - - onFinish(): void { - for (const id of this.dataMap.keys()) { - const data = this.dataMap.get(id); - const event = this.eventMap.get(id); - console.log(`Command ${id} data:`, data); - console.log(`Command ${id} event:`, event); - } - } - - onCommand(id: number, data: CommandData | CatterErr): Action { - this.dataMap.set(id, data); - return { type: 'skip' }; - } - - onExecution(id: number, event: ExecutionEvent): void { - this.eventMap.set(id, event); - } -} - -register(new MyCatterPlugin()); -``` +TODO \ No newline at end of file From 4790b19ed2a69ba04b1c95ccd9886fd5507b848e Mon Sep 17 00:00:00 2001 From: seele Date: Wed, 18 Mar 2026 21:02:48 +0800 Subject: [PATCH 6/9] refactor: rename to_string to format for improved error message handling --- doc/api.md | 2 +- src/catter/core/js.cc | 4 ++-- src/catter/core/qjs.h | 4 ++-- tests/unit/catter/core/qjs.cc | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/api.md b/doc/api.md index 30404ce..1333ed7 100644 --- a/doc/api.md +++ b/doc/api.md @@ -1 +1 @@ -TODO \ No newline at end of file +TODO diff --git a/src/catter/core/js.cc b/src/catter/core/js.cc index 88f9504..e51f2ad 100644 --- a/src/catter/core/js.cc +++ b/src/catter/core/js.cc @@ -106,7 +106,7 @@ void sync_eval(std::string_view input, const char* filename, int eval_flags) { auto reject = CallBack::from(js_ctx, [&](qjs::Parameters args) { state = Rejected; for(auto& arg: args) { - error_strace += arg.as().to_string() + "\n"; + error_strace += arg.as().format() + "\n"; } }); @@ -118,7 +118,7 @@ void sync_eval(std::string_view input, const char* filename, int eval_flags) { state = Rejected; try { for(auto& arg: args) { - error_strace += arg.as().to_string() + "\n"; + error_strace += arg.as().format() + "\n"; } } catch(const std::exception& e) { error_strace += std::format("Exception: {}\n", e.what()); diff --git a/src/catter/core/qjs.h b/src/catter/core/qjs.h index 19f0e0b..457fb17 100644 --- a/src/catter/core/qjs.h +++ b/src/catter/core/qjs.h @@ -457,7 +457,7 @@ class Error : protected Object { return this->get_property("name").as(); } - std::string to_string() const { + std::string format() const { return std::format("{}: {}\nStack Trace:\n{}", this->name(), this->message(), @@ -465,7 +465,7 @@ class Error : protected Object { } }; -inline JSException::JSException(const Error& error) : Exception(error.to_string()) {} +inline JSException::JSException(const Error& error) : Exception(error.format()) {} inline JSException JSException::dump(JSContext* ctx) { return JSException(Error(ctx, JS_GetException(ctx))); diff --git a/tests/unit/catter/core/qjs.cc b/tests/unit/catter/core/qjs.cc index a61b045..1d8bbfb 100644 --- a/tests/unit/catter/core/qjs.cc +++ b/tests/unit/catter/core/qjs.cc @@ -506,7 +506,7 @@ TEST_SUITE(qjs_tests) { EXPECT_TRUE(error.name() == "TypeError"); EXPECT_TRUE(error.message() == "boom"); EXPECT_TRUE(error.stack().contains(":1:4")); - EXPECT_TRUE(error.to_string().contains("Stack Trace:")); + EXPECT_TRUE(error.format().contains("Stack Trace:")); auto parsed = qjs::json::parse(R"({"number":1,"text":"ok"})", ctx).as(); auto dumped = qjs::json::stringify(parsed); From ad836d74aeb6f5532e835cff181d2da69ce19fc5 Mon Sep 17 00:00:00 2001 From: seele Date: Wed, 18 Mar 2026 21:15:16 +0800 Subject: [PATCH 7/9] format --- tests/unit/common/opt/clang.cc | 47 +++++++++++++++------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/tests/unit/common/opt/clang.cc b/tests/unit/common/opt/clang.cc index 59bc12d..1bbd4d0 100644 --- a/tests/unit/common/opt/clang.cc +++ b/tests/unit/common/opt/clang.cc @@ -93,29 +93,24 @@ TEST_SUITE(clang_option_table_tests) { EXPECT_EQ(parsed.args[2].option_id.id(), opt::clang::ID_INPUT); EXPECT_EQ(parsed.args[2].get_spelling_view(), "-dash.cc"); }; - - TEST_CASE(parse_unknown_and_missing_value){ - {const auto argv = - std::to_array({"clang++", "--definitely-not-a-real-clang-flag"}); - - auto parsed = parse_command(argv); - - EXPECT_TRUE(parsed.errors.empty()); - ASSERT_EQ(parsed.args.size(), 1U); - EXPECT_EQ(parsed.args[0].option_id.id(), opt::clang::ID_UNKNOWN); - EXPECT_EQ(parsed.args[0].get_spelling_view(), "--definitely-not-a-real-clang-flag"); -} - -{ - const auto argv = std::to_array({"clang++", "-o"}); - - auto parsed = parse_command(argv); - - EXPECT_TRUE(parsed.args.empty()); - ASSERT_EQ(parsed.errors.size(), 1U); - EXPECT_TRUE(parsed.errors[0].contains("missing argument value")); -} -} -; -} -; + TEST_CASE(parse_unknown_and_missing_value) { + + { + const auto argv = + std::to_array({"clang++", "--definitely-not-a-real-clang-flag"}); + auto parsed = parse_command(argv); + EXPECT_TRUE(parsed.errors.empty()); + ASSERT_EQ(parsed.args.size(), 1U); + EXPECT_EQ(parsed.args[0].option_id.id(), opt::clang::ID_UNKNOWN); + EXPECT_EQ(parsed.args[0].get_spelling_view(), "--definitely-not-a-real-clang-flag"); + }; + + { + const auto argv = std::to_array({"clang++", "-o"}); + auto parsed = parse_command(argv); + EXPECT_TRUE(parsed.args.empty()); + ASSERT_EQ(parsed.errors.size(), 1U); + EXPECT_TRUE(parsed.errors[0].contains("missing argument value")); + }; + } +}; From 9d808134bce6268a56e1766ab37f01d5c94a7193 Mon Sep 17 00:00:00 2001 From: seele Date: Wed, 18 Mar 2026 21:52:03 +0800 Subject: [PATCH 8/9] format --- src/catter/core/js.cc | 4 ++-- src/catter/core/qjs.h | 4 ++-- tests/unit/catter/core/qjs.cc | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/catter/core/js.cc b/src/catter/core/js.cc index e51f2ad..88f9504 100644 --- a/src/catter/core/js.cc +++ b/src/catter/core/js.cc @@ -106,7 +106,7 @@ void sync_eval(std::string_view input, const char* filename, int eval_flags) { auto reject = CallBack::from(js_ctx, [&](qjs::Parameters args) { state = Rejected; for(auto& arg: args) { - error_strace += arg.as().format() + "\n"; + error_strace += arg.as().to_string() + "\n"; } }); @@ -118,7 +118,7 @@ void sync_eval(std::string_view input, const char* filename, int eval_flags) { state = Rejected; try { for(auto& arg: args) { - error_strace += arg.as().format() + "\n"; + error_strace += arg.as().to_string() + "\n"; } } catch(const std::exception& e) { error_strace += std::format("Exception: {}\n", e.what()); diff --git a/src/catter/core/qjs.h b/src/catter/core/qjs.h index 457fb17..19f0e0b 100644 --- a/src/catter/core/qjs.h +++ b/src/catter/core/qjs.h @@ -457,7 +457,7 @@ class Error : protected Object { return this->get_property("name").as(); } - std::string format() const { + std::string to_string() const { return std::format("{}: {}\nStack Trace:\n{}", this->name(), this->message(), @@ -465,7 +465,7 @@ class Error : protected Object { } }; -inline JSException::JSException(const Error& error) : Exception(error.format()) {} +inline JSException::JSException(const Error& error) : Exception(error.to_string()) {} inline JSException JSException::dump(JSContext* ctx) { return JSException(Error(ctx, JS_GetException(ctx))); diff --git a/tests/unit/catter/core/qjs.cc b/tests/unit/catter/core/qjs.cc index 1d8bbfb..4bf38d3 100644 --- a/tests/unit/catter/core/qjs.cc +++ b/tests/unit/catter/core/qjs.cc @@ -1,4 +1,3 @@ -#include #include #include #include From 4a3b903892e28e8df9f8e9954417f6e0e8d9635f Mon Sep 17 00:00:00 2001 From: seele Date: Wed, 18 Mar 2026 22:48:46 +0800 Subject: [PATCH 9/9] refactor: rename to_string to format for improved error handling in qjs --- src/catter/core/js.cc | 4 ++-- src/catter/core/qjs.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/catter/core/js.cc b/src/catter/core/js.cc index 88f9504..e51f2ad 100644 --- a/src/catter/core/js.cc +++ b/src/catter/core/js.cc @@ -106,7 +106,7 @@ void sync_eval(std::string_view input, const char* filename, int eval_flags) { auto reject = CallBack::from(js_ctx, [&](qjs::Parameters args) { state = Rejected; for(auto& arg: args) { - error_strace += arg.as().to_string() + "\n"; + error_strace += arg.as().format() + "\n"; } }); @@ -118,7 +118,7 @@ void sync_eval(std::string_view input, const char* filename, int eval_flags) { state = Rejected; try { for(auto& arg: args) { - error_strace += arg.as().to_string() + "\n"; + error_strace += arg.as().format() + "\n"; } } catch(const std::exception& e) { error_strace += std::format("Exception: {}\n", e.what()); diff --git a/src/catter/core/qjs.h b/src/catter/core/qjs.h index 19f0e0b..457fb17 100644 --- a/src/catter/core/qjs.h +++ b/src/catter/core/qjs.h @@ -457,7 +457,7 @@ class Error : protected Object { return this->get_property("name").as(); } - std::string to_string() const { + std::string format() const { return std::format("{}: {}\nStack Trace:\n{}", this->name(), this->message(), @@ -465,7 +465,7 @@ class Error : protected Object { } }; -inline JSException::JSException(const Error& error) : Exception(error.to_string()) {} +inline JSException::JSException(const Error& error) : Exception(error.format()) {} inline JSException JSException::dump(JSContext* ctx) { return JSException(Error(ctx, JS_GetException(ctx)));