Skip to content
Merged
19 changes: 11 additions & 8 deletions cjs/deprecation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ struct hash<DeprecationEntry> {
static std::unordered_set<DeprecationEntry> logged_messages;

GJS_JSAPI_RETURN_CONVENTION
static JS::UniqueChars get_callsite(JSContext* cx) {
static JS::UniqueChars get_callsite(JSContext* cx, unsigned max_frames) {
JS::RootedObject stack_frame(cx);
if (!JS::CaptureCurrentStack(cx, &stack_frame,
JS::StackCapture(JS::MaxFrames(1))) ||
JS::StackCapture(JS::MaxFrames(max_frames))) ||
!stack_frame)
return nullptr;

Expand All @@ -98,8 +98,9 @@ static JS::UniqueChars get_callsite(JSContext* cx) {

static void warn_deprecated_unsafe_internal(JSContext* cx,
const GjsDeprecationMessageId id,
const char* msg) {
JS::UniqueChars callsite(get_callsite(cx));
const char* msg,
unsigned max_frames) {
JS::UniqueChars callsite(get_callsite(cx, max_frames));
DeprecationEntry entry(id, callsite.get());
if (!logged_messages.count(entry)) {
JS::UniqueChars stack_dump =
Expand All @@ -113,13 +114,15 @@ static void warn_deprecated_unsafe_internal(JSContext* cx,
* stack dump API and not the "safe" gjs_dumpstack() which can only print to
* stdout or stderr. Do not use this function during GC, for example. */
void _gjs_warn_deprecated_once_per_callsite(JSContext* cx,
const GjsDeprecationMessageId id) {
warn_deprecated_unsafe_internal(cx, id, messages[id]);
const GjsDeprecationMessageId id,
unsigned max_frames) {
warn_deprecated_unsafe_internal(cx, id, messages[id], max_frames);
}

void _gjs_warn_deprecated_once_per_callsite(
JSContext* cx, GjsDeprecationMessageId id,
const std::vector<const char*>& args) {
const std::vector<const char*>& args,
unsigned max_frames) {
// In C++20, use std::format() for this
std::string_view format_string{messages[id]};
std::stringstream message;
Expand Down Expand Up @@ -149,5 +152,5 @@ void _gjs_warn_deprecated_once_per_callsite(
message << format_string.substr(copied, std::string::npos);

std::string message_formatted = message.str();
warn_deprecated_unsafe_internal(cx, id, message_formatted.c_str());
warn_deprecated_unsafe_internal(cx, id, message_formatted.c_str(), max_frames);
}
5 changes: 3 additions & 2 deletions cjs/deprecation.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ enum GjsDeprecationMessageId : unsigned {
};

void _gjs_warn_deprecated_once_per_callsite(JSContext* cx,
GjsDeprecationMessageId message);
GjsDeprecationMessageId message,
unsigned max_frames = 1);

void _gjs_warn_deprecated_once_per_callsite(
JSContext* cx, GjsDeprecationMessageId id,
const std::vector<const char*>& args);
const std::vector<const char*>& args, unsigned max_frames = 1);

#endif // GJS_DEPRECATION_H_
14 changes: 14 additions & 0 deletions gi/function.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ class Function : public CWrapper<Function> {
GJS_JSAPI_RETURN_CONVENTION
static bool get_length(JSContext* cx, unsigned argc, JS::Value* vp);

GJS_JSAPI_RETURN_CONVENTION
static bool get_name(JSContext* cx, unsigned argc, JS::Value* vp);

GJS_JSAPI_RETURN_CONVENTION
static bool to_string(JSContext* cx, unsigned argc, JS::Value* vp);

Expand Down Expand Up @@ -1251,6 +1254,16 @@ bool Function::get_length(JSContext* cx, unsigned argc, JS::Value* vp) {
return true;
}

bool Function::get_name(JSContext* cx, unsigned argc, JS::Value* vp) {
GJS_CHECK_WRAPPER_PRIV(cx, argc, vp, rec, this_obj, Function, priv);

if (priv->m_info.type() == GI_INFO_TYPE_FUNCTION)
return gjs_string_from_utf8(cx, g_function_info_get_symbol(priv->m_info),
rec.rval());

return gjs_string_from_utf8(cx, priv->format_name().c_str(), rec.rval());
}

bool Function::to_string(JSContext* context, unsigned argc, JS::Value* vp) {
GJS_CHECK_WRAPPER_PRIV(context, argc, vp, rec, this_obj, Function, priv);
return priv->to_string_impl(context, rec.rval());
Expand Down Expand Up @@ -1302,6 +1315,7 @@ const JSClassOps Function::class_ops = {

const JSPropertySpec Function::proto_props[] = {
JS_PSG("length", &Function::get_length, JSPROP_PERMANENT),
JS_PSG("name", &Function::get_name, JSPROP_PERMANENT),
JS_STRING_SYM_PS(toStringTag, "GIRepositoryFunction", JSPROP_READONLY),
JS_PS_END};

Expand Down
86 changes: 86 additions & 0 deletions installed-tests/js/testGio.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

const {GLib, Gio, GObject} = imports.gi;

let GioUnix;
try {
GioUnix = imports.gi.GioUnix;
} catch {}

const Foo = GObject.registerClass({
Properties: {
boolval: GObject.ParamSpec.boolean('boolval', '', '',
Expand Down Expand Up @@ -390,6 +395,87 @@ describe('Gio.FileEnumerator overrides', function () {
});
});

describe('Gio.DesktopAppInfo fallback', function () {
const requiredVersion =
GLib.MAJOR_VERSION > 2 ||
(GLib.MAJOR_VERSION === 2 && GLib.MINOR_VERSION >= 86);
let keyFile;
const desktopFileContent = `[Desktop Entry]
Version=1.0
Type=Application
Name=Some Application
Exec=${GLib.find_program_in_path('sh')}
`;
beforeAll(function () {
// Set up log writer for tests to override
keyFile = new GLib.KeyFile();
keyFile.load_from_data(desktopFileContent, desktopFileContent.length,
GLib.KeyFileFlags.NONE);
});

beforeEach(function () {
if (!GioUnix)
pending('Not supported platform');

if (!requiredVersion)
pending('Installed Gio is not new enough for this test');
});

function expectDeprecationWarning(testFunction) {
if (!requiredVersion)
pending('Installed Gio is not new enough for this test');

GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING,
'*Gio.DesktopAppInfo has been moved to a separate platform-specific library. ' +
'Please update your code to use GioUnix.DesktopAppInfo instead*');
testFunction();
GLib.test_assert_expected_messages_internal('Cjs', 'testGio.js', 0,
'Gio.DesktopAppInfo expectWarnsOnNewerGio');
}

it('can be created using GioUnix', function () {
expect(GioUnix.DesktopAppInfo.new_from_keyfile(keyFile)).not.toBeNull();
});

it('can be created using Gio wrapper', function () {
expectDeprecationWarning(() =>
expect(Gio.DesktopAppInfo.new_from_keyfile(keyFile)).not.toBeNull());
expectDeprecationWarning(() =>
expect(Gio.DesktopAppInfo.new_from_keyfile(keyFile)).not.toBeNull());
});

describe('provides platform-independent functions', function () {
[Gio, GioUnix].forEach(ns => it(`when created from ${ns.__name__}`, function () {
if (!requiredVersion)
pending('Installed Gio is not new enough for this test');

const maybeExpectDeprecationWarning = ns === Gio
? expectDeprecationWarning : tf => tf();

maybeExpectDeprecationWarning(() => {
const appInfo = ns.DesktopAppInfo.new_from_keyfile(keyFile);
expect(appInfo.get_name()).toBe('Some Application');
});
}));
});

describe('provides unix-only functions', function () {
[Gio, GioUnix].forEach(ns => it(`when created from ${ns.__name__}`, function () {
if (!requiredVersion)
pending('Installed Gio is not new enough for this test');

const maybeExpectDeprecationWarning = ns === Gio
? expectDeprecationWarning : tf => tf();

maybeExpectDeprecationWarning(() => {
const appInfo = ns.DesktopAppInfo.new_from_keyfile(keyFile);
expect(appInfo.has_key('Name')).toBeTrue();
expect(appInfo.get_string('Name')).toBe('Some Application');
});
}));
});
});

describe('Non-introspectable file attribute overrides', function () {
let numExpectedWarnings, file, info;
const flags = [Gio.FileQueryInfoFlags.NONE, null];
Expand Down
16 changes: 16 additions & 0 deletions installed-tests/js/testIntrospection.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,22 @@ describe('Backwards compatibility for GLib/Gio platform specific GIRs', function
'Expected deprecation message for Gio.Unix -> GioUnix');
});

it('GioUnix functions are looked up in GioUnix, not Gio', function () {
if (skip) {
pending('GioUnix required for this test');
return;
}

GLib.test_expect_message('Cjs', GLib.LogLevelFlags.LEVEL_WARNING,
'*Gio.unix_mounts_get*GioUnix.mounts_get*instead*');

expect(imports.gi.Gio.unix_mounts_get.name).toBe('g_unix_mounts_get');

GLib.test_assert_expected_messages_internal('Cjs',
'testIntrospection.js', 0,
'Expected deprecation message for Gio.Unix -> GioUnix');
});

it("doesn't print the message if the type isn't resolved directly", function () {
if (skip) {
pending('GioUnix required for this test');
Expand Down
47 changes: 47 additions & 0 deletions modules/core/overrides/Gio.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
var GLib = imports.gi.GLib;
var CjsPrivate = imports.gi.CjsPrivate;
var Signals = imports.signals;
const { warnDeprecatedOncePerCallsite, PLATFORM_SPECIFIC_TYPELIB } = imports._print;
var Gio;

// Ensures that a Gio.UnixFDList being passed into or out of a DBus method with
Expand Down Expand Up @@ -479,9 +480,55 @@ function _warnNotIntrospectable(funcName, replacement) {

function _init() {
Gio = this;
let GioPlatform = {};

Gio.Application.prototype.runAsync = GLib.MainLoop.prototype.runAsync;

if (GLib.MAJOR_VERSION > 2 ||
(GLib.MAJOR_VERSION === 2 && GLib.MINOR_VERSION >= 86)) {
// Redefine Gio functions with platform-specific implementations to be
// backward compatible with gi-repository 1.0, however when possible we
// notify a deprecation warning, to ensure that the surrounding code is
// updated.
try {
GioPlatform = imports.gi.GioUnix;
} catch {
try {
GioPlatform = imports.gi.GioWin32;
} catch {}
}
}

const platformName = `${GioPlatform?.__name__?.slice(3 /* 'Gio'.length */)}`;
const platformNameLower = platformName.toLowerCase();
Object.entries(Object.getOwnPropertyDescriptors(GioPlatform)).forEach(([prop, desc]) => {
let genericProp = prop;

const originalValue = GioPlatform[prop];
const gtypeName = originalValue.$gtype?.name;
if (gtypeName?.startsWith(`G${platformName}`))
genericProp = `${platformName}${prop}`;
else if (originalValue instanceof Function &&
originalValue.name.startsWith(`g_${platformNameLower}_`))
genericProp = `${platformNameLower}_${prop}`;

if (Object.hasOwn(Gio, genericProp)) {
console.debug(`Gio already contains property ${genericProp}`);
Gio[genericProp] = originalValue;
return;
}

Object.defineProperty(Gio, genericProp, {
enumerable: true,
configurable: false,
get() {
warnDeprecatedOncePerCallsite(PLATFORM_SPECIFIC_TYPELIB,
`Gio.${genericProp}`, `${GioPlatform.__name__}.${prop}`);
return desc.get?.() ?? desc.value;
},
});
});

Gio.DBus = {
// Namespace some functions
get: Gio.bus_get,
Expand Down
58 changes: 57 additions & 1 deletion modules/print.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

#include <config.h>

#include <stddef.h> // for size_t
#include <stdint.h>

#include <string>
#include <vector>

#include <glib.h>

Expand All @@ -21,6 +25,7 @@
#include <js/Value.h>
#include <jsapi.h> // for JS_NewPlainObject

#include "cjs/deprecation.h"
#include "cjs/global.h"
#include "cjs/jsapi-util.h"
#include "cjs/macros.h"
Expand Down Expand Up @@ -182,6 +187,48 @@ static bool get_pretty_print_function(JSContext*, unsigned argc,
return true;
}

GJS_JSAPI_RETURN_CONVENTION
static bool warn_deprecated_once_per_callsite(JSContext* cx, unsigned argc,
JS::Value* vp) {
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);

g_assert(args.length() >= 1 &&
"warnDeprecatedOncePerCallsite takes at least 1 argument");

g_assert(
args[0].isInt32() &&
"warnDeprecatedOncePerCallsite argument 1 must be a message ID number");
int32_t message_id = args[0].toInt32();
g_assert(
message_id >= 0 &&
uint32_t(message_id) < GjsDeprecationMessageId::LastValue &&
"warnDeprecatedOncePerCallsite argument 1 must be a message ID number");

if (args.length() == 1) {
_gjs_warn_deprecated_once_per_callsite(
cx, GjsDeprecationMessageId(message_id), 2);
return true;
}

std::vector<std::string> format_args_str;
std::vector<const char*> format_args;
for (size_t ix = 1; ix < args.length(); ix++) {
g_assert(args[ix].isString() &&
"warnDeprecatedOncePerCallsite subsequent arguments must be "
"strings");
JS::RootedString v_format_arg{cx, args[ix].toString()};
JS::UniqueChars format_arg = JS_EncodeStringToUTF8(cx, v_format_arg);
if (!format_arg)
return false;
format_args_str.emplace_back(format_arg.get());
format_args.emplace_back(format_args_str.back().c_str());
}

_gjs_warn_deprecated_once_per_callsite(
cx, GjsDeprecationMessageId(message_id), format_args, 2);
return true;
}

// clang-format off
static constexpr JSFunctionSpec funcs[] = {
JS_FN("log", gjs_log, 1, GJS_MODULE_PROP_FLAGS),
Expand All @@ -190,13 +237,22 @@ static constexpr JSFunctionSpec funcs[] = {
JS_FN("printerr", gjs_printerr, 0, GJS_MODULE_PROP_FLAGS),
JS_FN("setPrettyPrintFunction", set_pretty_print_function, 1, GJS_MODULE_PROP_FLAGS),
JS_FN("getPrettyPrintFunction", get_pretty_print_function, 1, GJS_MODULE_PROP_FLAGS),
JS_FN("warnDeprecatedOncePerCallsite", warn_deprecated_once_per_callsite, 1,
GJS_MODULE_PROP_FLAGS),
JS_FS_END};
// clang-format on

static constexpr JSPropertySpec props[] = {
JSPropertySpec::int32Value("PLATFORM_SPECIFIC_TYPELIB",
GJS_MODULE_PROP_FLAGS,
GjsDeprecationMessageId::PlatformSpecificTypelib),
JS_PS_END};

bool gjs_define_print_stuff(JSContext* context,
JS::MutableHandleObject module) {
module.set(JS_NewPlainObject(context));
if (!module)
return false;
return JS_DefineFunctions(context, module, funcs);
return JS_DefineFunctions(context, module, funcs) &&
JS_DefineProperties(context, module, props);
}
2 changes: 1 addition & 1 deletion modules/script/package.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ function checkSymbol(lib, ver, symbol) {
// GObject property
let pspec = null;
if (GObject.type_is_a(obj.$gtype, GObject.TYPE_INTERFACE)) {
let iface = GObject.type_default_interface_ref(obj.$gtype);
let iface = GObject.type_default_interface_get(obj.$gtype);
pspec = GObject.Object.interface_find_property(iface, sym);
} else if (GObject.type_is_a(obj.$gtype, GObject.TYPE_OBJECT)) {
pspec = GObject.Object.find_property.call(obj.$gtype, sym);
Expand Down
Loading