After an evaluation, GNOME has moved from Bugzilla to GitLab. Learn more about GitLab.
No new issues can be reported in GNOME Bugzilla anymore.
To report an issue in a GNOME project, go to GNOME GitLab.
Do not go to GNOME Gitlab for: Bluefish, Doxygen, GnuCash, GStreamer, java-gnome, LDTP, NetworkManager, Tomboy.
Bug 785652 - Use ES6 classes
Use ES6 classes
Status: RESOLVED FIXED
Product: gjs
Classification: Bindings
Component: general
1.49.x
Other Linux
: Normal enhancement
: ---
Assigned To: Philip Chimento
gjs-maint
Depends on:
Blocks:
 
 
Reported: 2017-07-31 17:32 UTC by Philip Chimento
Modified: 2017-08-11 10:36 UTC
See Also:
GNOME target: ---
GNOME version: ---


Attachments
Revert "js: Workaround for function with custom prototype" (16.61 KB, patch)
2017-07-31 17:36 UTC, Philip Chimento
committed Details | Review
Revert "build: Allow compiling without RTTI" (1.34 KB, patch)
2017-07-31 17:36 UTC, Philip Chimento
committed Details | Review
class: Stop using custom __name__ property (11.08 KB, patch)
2017-07-31 17:36 UTC, Philip Chimento
none Details | Review
lang: Move all legacy Lang.Class code (51.25 KB, patch)
2017-07-31 17:36 UTC, Philip Chimento
none Details | Review
tests: Add ES6 class inheriting from legacy class (2.95 KB, patch)
2017-07-31 17:36 UTC, Philip Chimento
none Details | Review
class: Move to ES6 classes in internal code (9.64 KB, patch)
2017-07-31 17:36 UTC, Philip Chimento
none Details | Review
GObject: Move all legacy GObject class code (42.01 KB, patch)
2017-07-31 17:36 UTC, Philip Chimento
none Details | Review
class: Stop using custom __name__ property (11.57 KB, patch)
2017-08-03 21:47 UTC, Philip Chimento
reviewed Details | Review
lang: Move all legacy Lang.Class code (51.25 KB, patch)
2017-08-03 21:47 UTC, Philip Chimento
committed Details | Review
class: Move to ES6 classes in internal code (13.99 KB, patch)
2017-08-03 21:47 UTC, Philip Chimento
committed Details | Review
GObject: Move all legacy GObject class code (78.54 KB, patch)
2017-08-03 21:47 UTC, Philip Chimento
committed Details | Review
GObject: Adapt GObject class framework to ES6 (37.09 KB, patch)
2017-08-03 21:48 UTC, Philip Chimento
committed Details | Review
class: Move to ES6 GObject classes in internal code (6.84 KB, patch)
2017-08-03 21:48 UTC, Philip Chimento
committed Details | Review
Gtk: Use GObject.registerClass() for Gtk.Widgets (9.01 KB, patch)
2017-08-03 21:48 UTC, Philip Chimento
none Details | Review
tests: Add ES6 class inheriting from legacy class (7.47 KB, patch)
2017-08-03 21:48 UTC, Philip Chimento
committed Details | Review
Gtk: Use GObject.registerClass() for Gtk.Widgets (8.79 KB, patch)
2017-08-08 15:35 UTC, Philip Chimento
committed Details | Review
legacy class: Add name property to class object (5.54 KB, patch)
2017-08-08 15:35 UTC, Philip Chimento
committed Details | Review
legacy class: Reinstate Lang.getMetaClass() (1.56 KB, patch)
2017-08-09 14:15 UTC, Philip Chimento
committed Details | Review

Description Philip Chimento 2017-07-31 17:32:37 UTC
See https://ptomato.wordpress.com/2017/07/14/inventing-gobject-es6-classes/ for background.

It should be possible to use ES6 classes instead of Lang.Class, for both Javascript-only classes and GObject classes. It should also be backwards compatible with existing code; new classes should be able to inherit from old classes.
Comment 1 Philip Chimento 2017-07-31 17:36:21 UTC
Created attachment 356666 [details] [review]
Revert "js: Workaround for function with custom prototype"

This reverts commit 72c0298c5f9df9036ed67fd504db84cbc028daaa.
The warning that was the reason for this code in the first place was
removed in SpiderMonkey 52. According to
https://bugzilla.mozilla.org/show_bug.cgi?id=1049041 the warning was out
of date, and there are only performance effects "if the object escapes to
somewhere interesting before getting its final __proto__".

In addition, client code should move to ES6 classes which do not have
this performance problem.

Edited to resolve conflicts due to code being moved around in the
meantime, and to use Object.setPrototypeOf() instead of the deprecated
__proto__ property.
Comment 2 Philip Chimento 2017-07-31 17:36:26 UTC
Created attachment 356667 [details] [review]
Revert "build: Allow compiling without RTTI"

This reverts commit eccf3a14b9b0d6aae89baeaa32dd53f07b2b77d7.

We don't need to match SpiderMonkey here anymore, since we don't inherit
from a SpiderMonkey-defined class.
Comment 3 Philip Chimento 2017-07-31 17:36:31 UTC
Created attachment 356668 [details] [review]
class: Stop using custom __name__ property

Since ES6 classes are functions, if you declare 'class Foo {}' then
'Foo.name === "Foo"'. We had a custom __name__ property on Lang.Class
instances, but instead mimic the normal JS class and put a name property
on the class.

That is, the equivalent to 'instance.__name__' is now
'instance.constructor.name'.

This could potentially break client code if any was using this, but the
double underscore made it clear enough that the property should have been
internal.
Comment 4 Philip Chimento 2017-07-31 17:36:37 UTC
Created attachment 356669 [details] [review]
lang: Move all legacy Lang.Class code

This moves Lang.Class into a separate Legacy module, which is not meant
to be imported directly; instead, Lang imports it and exports the same
members that it always did.

Also moves all tests for the legacy Lang.Class into a separate file. It
is named testLegacyClass in order to indicate that it tests legacy code.
Comment 5 Philip Chimento 2017-07-31 17:36:42 UTC
Created attachment 356670 [details] [review]
tests: Add ES6 class inheriting from legacy class

This should be supported, so add tests for it.
Comment 6 Philip Chimento 2017-07-31 17:36:48 UTC
Created attachment 356671 [details] [review]
class: Move to ES6 classes in internal code

Where possible, move usage of Lang.Class within GJS to use ES6 classes.
That is currently possible for any classes which don't inherit from
GObject classes and don't implement Lang.Interfaces.

Remove the documentation for the Lang.Class class framework, as it's now
become outdated.
Comment 7 Philip Chimento 2017-07-31 17:36:54 UTC
Created attachment 356672 [details] [review]
GObject: Move all legacy GObject class code

This moves the GObject.Class and GObject.Interface code into the Legacy
module along with Lang.Class and Lang.Interface.

Also moves all tests for the legacy GObject code into a separate file.
It is named testLegacyGObject in order to indicate that it tests legacy
code.
Comment 8 Cosimo Cecchi 2017-08-03 14:27:40 UTC
Review of attachment 356666 [details] [review]:

Nice
Comment 9 Cosimo Cecchi 2017-08-03 14:28:03 UTC
Review of attachment 356667 [details] [review]:

OK
Comment 10 Cosimo Cecchi 2017-08-03 14:35:51 UTC
Review of attachment 356668 [details] [review]:

Looks good, but the __name__ removal should probably be documented in the NEWS file anyway.

::: modules/lang.js
@@ +435,3 @@
     if (unfulfilledReqs.length > 0) {
         throw new Error('The following interfaces must be implemented before ' +
+            `${this.constructor.name}: ${unfulfilledReqs.join(', ')}`);

Should this.prototype.constructor.name not be used here instead?

@@ +443,3 @@
     .filter((p) => !(p in proto) || proto[p] === Interface.UNIMPLEMENTED);
     if (unimplementedFns.length > 0)
+        throw new Error(`The following members of ${this.constructor.name}` +

Ditto
Comment 11 Philip Chimento 2017-08-03 21:47:31 UTC
Created attachment 356893 [details] [review]
class: Stop using custom __name__ property

Since ES6 classes are functions, if you declare 'class Foo {}' then
'Foo.name === "Foo"'. We had a custom __name__ property on Lang.Class
instances, but instead mimic the normal JS class and put a name property
on the class.

That is, the equivalent to 'instance.__name__' is now
'instance.constructor.name'.

This could potentially break client code if any was using this, but the
double underscore made it clear enough that the property should have been
internal.
Comment 12 Philip Chimento 2017-08-03 21:47:38 UTC
Created attachment 356894 [details] [review]
lang: Move all legacy Lang.Class code

This moves Lang.Class into a separate Legacy module, which is not meant
to be imported directly; instead, Lang imports it and exports the same
members that it always did.

Also moves all tests for the legacy Lang.Class into a separate file. It
is named testLegacyClass in order to indicate that it tests legacy code.
Comment 13 Philip Chimento 2017-08-03 21:47:46 UTC
Created attachment 356895 [details] [review]
class: Move to ES6 classes in internal code

Where possible, move usage of Lang.Class within GJS to use ES6 classes.
That is currently possible for any classes which don't inherit from
GObject classes and don't implement Lang.Interfaces.

Remove the documentation for the Lang.Class class framework, as it's now
become outdated.
Comment 14 Philip Chimento 2017-08-03 21:47:54 UTC
Created attachment 356896 [details] [review]
GObject: Move all legacy GObject class code

This moves the GObject.Class and GObject.Interface code into the Legacy
module along with Lang.Class and Lang.Interface.

Also moves all tests for the legacy GObject code into a separate file.
It is named testLegacyGObject in order to indicate that it tests legacy
code.
Comment 15 Philip Chimento 2017-08-03 21:48:01 UTC
Created attachment 356897 [details] [review]
GObject: Adapt GObject class framework to ES6

This moves our GObject class framework to use ES6 classes. GObject.Object
and GObject.Interface gain static _classInit() methods which register
GTypes and perform other such magic that used to be performed in the
metaclasses. When defining the class you must call
GObject.registerClass() on the class object, with an optional metadata
object as the first argument. (The metadata object works exactly like the
meta properties from Lang.Class, except that Name and Extends are not
present.)

Old:

    var MyClass = new Lang.Class({
        Name: 'MyClass',
        Extends: GObject.Object,
        Signals: { 'event': {} },
        _init(props={}) {
            this._private = [];
            this.parent(props);
        },
    });

New:

    var MyClass = GObject.registerClass({
        Signals: { 'event': {} },
    }, class MyClass extends GObject.Object {
        _init(props={}) {
            this._private = [];
            super._init(props);
        }
    });

It is forward compatible with the following syntax requiring decorators
and class fields, which are not in the JS standard yet:

    @GObject.registerClass
    class MyClass extends GObject.Object {
        static [GObject.signals] = { 'event': {} }
        _init(props={}) {
            this._private = [];
            super._init(props);
        }
    }

We also now add a [Symbol.hasInstance]() method to interfaces so that
instanceof will finally work for interfaces.

One limitation is that GObject ES6 classes can't have constructor()
methods, they must do any setup in an _init() method.
Comment 16 Philip Chimento 2017-08-03 21:48:07 UTC
Created attachment 356898 [details] [review]
class: Move to ES6 GObject classes in internal code

This moves all remaining uses of Lang.Class (except for where the legacy
class framework is explicitly tested) from the codebase, in favour of
GObject.registerClass().
Comment 17 Philip Chimento 2017-08-03 21:48:16 UTC
Created attachment 356899 [details] [review]
Gtk: Use GObject.registerClass() for Gtk.Widgets

This extends the GObject.registerClass() API to deal with the magic
properties from Gtk.WidgetClass: CssName, Template, Children, and
InternalChildren.
Comment 18 Philip Chimento 2017-08-03 21:48:22 UTC
Created attachment 356900 [details] [review]
tests: Add ES6 class inheriting from legacy class

This should be supported, so add tests for it.
Comment 19 Philip Chimento 2017-08-03 21:49:44 UTC
(In reply to Cosimo Cecchi from comment #10)
> Review of attachment 356668 [details] [review] [review]:
> ::: modules/lang.js
> @@ +435,3 @@
>      if (unfulfilledReqs.length > 0) {
>          throw new Error('The following interfaces must be implemented
> before ' +
> +            `${this.constructor.name}: ${unfulfilledReqs.join(', ')}`);
> 
> Should this.prototype.constructor.name not be used here instead?

I don't believe so; this.prototype.constructor === this, by definition.
Comment 20 Cosimo Cecchi 2017-08-05 17:53:13 UTC
Review of attachment 356893 [details] [review]:

::: modules/lang.js
@@ +443,3 @@
     .filter((p) => !(p in proto) || proto[p] === Interface.UNIMPLEMENTED);
     if (unimplementedFns.length > 0)
+        throw new Error(`The following members of ${this.constructor.name}` +

What I meant is that elsewhere you have this replacement:
- this.__name__ -> this.constructor.name
- this.prototype.__name__ -> this.prototype.constructor.name

However here you're changing this.prototype.__name__ -> this.constructor.name, where I'd expect this.prototype.constructor.name to be used instead.
Comment 21 Cosimo Cecchi 2017-08-05 17:55:13 UTC
Review of attachment 356894 [details] [review]:

OK
Comment 22 Cosimo Cecchi 2017-08-05 17:56:44 UTC
Review of attachment 356895 [details] [review]:

OK
Comment 23 Cosimo Cecchi 2017-08-05 17:57:58 UTC
Review of attachment 356896 [details] [review]:

OK
Comment 24 Cosimo Cecchi 2017-08-05 18:18:39 UTC
Review of attachment 356897 [details] [review]:

Nice!

::: modules/overrides/GObject.js
@@ +31,3 @@
+var properties = Symbol('GObject properties');
+var signals = Symbol('GObject signals');
+var requires = Symbol('GObject interface requires');

Nit: alphabetical order
Comment 25 Cosimo Cecchi 2017-08-05 18:19:41 UTC
Review of attachment 356898 [details] [review]:

OK
Comment 26 Cosimo Cecchi 2017-08-05 18:23:17 UTC
Review of attachment 356899 [details] [review]:

::: installed-tests/js/testGtk.js
@@ +44,3 @@
+// Sadly, putting this in the body of the class will prevent calling
+// get_template_child, since MyComplexGtkSubclass will be bound to the ES6
+// class name without the GObject goodies in it

Not sure I fully understand this... Will it be a problem in practice for other GTK classes out there?
Comment 27 Cosimo Cecchi 2017-08-05 18:24:02 UTC
Review of attachment 356900 [details] [review]:

OK
Comment 28 Philip Chimento 2017-08-07 11:07:29 UTC
Attachment 356894 [details] pushed as 78a021e - lang: Move all legacy Lang.Class code
Attachment 356895 [details] pushed as 4cbfe50 - class: Move to ES6 classes in internal code
Attachment 356896 [details] pushed as bdb6667 - GObject: Move all legacy GObject class code
Attachment 356897 [details] pushed as 3017008 - GObject: Adapt GObject class framework to ES6
Attachment 356898 [details] pushed as 1fb1583 - class: Move to ES6 GObject classes in internal code
Attachment 356900 [details] pushed as 4c2719f - tests: Add ES6 class inheriting from legacy class
Comment 29 Philip Chimento 2017-08-07 11:09:22 UTC
(In reply to Cosimo Cecchi from comment #24)
> Review of attachment 356897 [details] [review] [review]:
> 
> Nice!
> 
> ::: modules/overrides/GObject.js
> @@ +31,3 @@
> +var properties = Symbol('GObject properties');
> +var signals = Symbol('GObject signals');
> +var requires = Symbol('GObject interface requires');
> 
> Nit: alphabetical order

OK, fixed. I'm half wondering if they should be ALL_CAPS, but this is more in line with Symbol.hasInstance and APIs like that. We can still change it in the .91 release if necessary, I think.
Comment 30 Philip Chimento 2017-08-07 11:10:12 UTC
(In reply to Cosimo Cecchi from comment #20)
> Review of attachment 356893 [details] [review] [review]:
> 
> ::: modules/lang.js
> @@ +443,3 @@
>      .filter((p) => !(p in proto) || proto[p] === Interface.UNIMPLEMENTED);
>      if (unimplementedFns.length > 0)
> +        throw new Error(`The following members of ${this.constructor.name}`
> +
> 
> What I meant is that elsewhere you have this replacement:
> - this.__name__ -> this.constructor.name
> - this.prototype.__name__ -> this.prototype.constructor.name
> 
> However here you're changing this.prototype.__name__ ->
> this.constructor.name, where I'd expect this.prototype.constructor.name to
> be used instead.

Hmm, I think I need to take an extra look at this, anyway, because this.prototype.constructor doesn't actually make sense.
Comment 31 Philip Chimento 2017-08-07 22:05:41 UTC
(In reply to Cosimo Cecchi from comment #26)
> Review of attachment 356899 [details] [review] [review]:
> 
> ::: installed-tests/js/testGtk.js
> @@ +44,3 @@
> +// Sadly, putting this in the body of the class will prevent calling
> +// get_template_child, since MyComplexGtkSubclass will be bound to the ES6
> +// class name without the GObject goodies in it
> 
> Not sure I fully understand this... Will it be a problem in practice for
> other GTK classes out there?

No, I don't believe so -- in practice they will use the Gtk.internalChildren symbol instead of calling get_template_child. Just seemed good to document it since it would have been supported before.

The more detailed explanation is that within a class body, the class name is bound locally to the ES6 class being defined. Since we have something like

const Foo = GObject.registerClass(class Foo {
   ...use(Foo)...
});

then "Foo" refers to the inner ES6 class expression, not the outer class object that is eventually bound to Foo in the rest of the program.

I'm sure that if we did this

const Foo = GObject.registerClass(class _FooInner {
    ...use(Foo)...
});

it would work correctly, though the class's name property would be set up incorrectly.
Comment 32 Philip Chimento 2017-08-08 15:35:10 UTC
Created attachment 357206 [details] [review]
Gtk: Use GObject.registerClass() for Gtk.Widgets

This extends the GObject.registerClass() API to deal with the magic
properties from Gtk.WidgetClass: CssName, Template, Children, and
InternalChildren.
Comment 33 Philip Chimento 2017-08-08 15:35:18 UTC
Created attachment 357207 [details] [review]
legacy class: Add name property to class object

Instead of the __name__ property on instances, add a name property to
class objects. The __name__ property continues to work, for backwards
compatibility in case any code used it (unlikely), but this is compatible
with ES6 classes.
Comment 34 Cosimo Cecchi 2017-08-08 20:56:35 UTC
Review of attachment 357206 [details] [review]:

OK
Comment 35 Cosimo Cecchi 2017-08-08 20:58:16 UTC
Review of attachment 357207 [details] [review]:

Looks good!
Comment 36 Philip Chimento 2017-08-09 09:59:55 UTC
Attachment 357206 [details] pushed as 2ede057 - Gtk: Use GObject.registerClass() for Gtk.Widgets
Attachment 357207 [details] pushed as 039738d - legacy class: Add name property to class object
Comment 37 Philip Chimento 2017-08-09 14:15:12 UTC
Created attachment 357269 [details] [review]
legacy class: Reinstate Lang.getMetaClass()

Some client code was using this, so we should keep it as public API. Add
a trivial test to make sure it doesn't break in the future.
Comment 38 Philip Chimento 2017-08-09 14:15:51 UTC
Fixed up a straggler.
Comment 39 Cosimo Cecchi 2017-08-09 21:14:00 UTC
Review of attachment 357269 [details] [review]:

OK
Comment 40 Philip Chimento 2017-08-11 10:35:59 UTC
Attachment 357269 [details] pushed as d9218eb - legacy class: Reinstate Lang.getMetaClass()