Skip to content

Commit

Permalink
Fix inheritance and constructor calls
Browse files Browse the repository at this point in the history
  • Loading branch information
aedart committed Feb 11, 2024
1 parent 195f094 commit 6786265
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 13 deletions.
23 changes: 23 additions & 0 deletions packages/support/src/mixins/mix.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { MixinFunction } from "@aedart/contracts/support/mixins";
import {Constructor} from "@aedart/contracts";

/**
* Mix target class with one or more abstract subclasses ("Mixins")
Expand Down Expand Up @@ -75,7 +76,29 @@ export function mix(...mixins: MixinFunction[])
*/
function extendTarget(target: object, superclass: object): object
{
// If the target does not extend another class...
if (Reflect.getPrototypeOf(target) === Reflect.getPrototypeOf(Function)) {
// Then the @mix() has created a new class that applies one or more mixins.
// However, their constructor(s) must be invoked (call to super()), which
// can only be done via a Proxy, by trapping calls to the constructor.

target = new Proxy(target, {
construct(target, argArray, newTarget): object
{
// Mimic call to super(), with target as the newTarget, for the superclass
// which has mixins applied.
Reflect.construct(superclass as Constructor, argArray, target as Constructor);

// Return new target instance...
return Reflect.construct(target as Constructor, argArray, newTarget);
}
});
}

// Ensure correct inheritance chain.
// @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor#changing_the_constructor_of_a_constructor_functions_prototype
Reflect.setPrototypeOf(target.prototype, superclass.prototype);
Reflect.setPrototypeOf(target, superclass);

return target;
}
Expand Down
173 changes: 160 additions & 13 deletions tests/browser/packages/support/mixins/mix.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,34 +157,159 @@ describe('@aedart/support/mixins', () => {
.toEqual(valueD);
});

// TODO: ...Hmmm, edge case that might not be that good!
xit('invokes constructors', () => {
it('mixin constructors are automatically invoked', () => {

const invoked = [];
const MyMixinA = Mixin((superclass) => class extends superclass {
constructor() {
super();
invoked.push('Mixin A');
}

getThis() {
return this;
}
});

const MyMixinB = Mixin((superclass) => class extends superclass {
constructor() {
super();
invoked.push('Mixin B');
}
});

@mix(
MyMixinA,
MyMixinB
)
class A {
constructor() {
// Class A has no immediate superclass. However, the @mix() class decorator
// extends it with a parent, which applies mixins. We SHOULD call super()
// here, but babel and other transpilers complain about doing such...
// THUS - the @mix() must somehow achieve this for us!

//super(); // THIS WILL NOT WORK HERE...
invoked.push('Class A');
}
}

// -------------------------------------------------------------------------- //

const instance = new A();

// Inheritance check
expect(instance instanceof A)
.withContext('should be instance of class A')
.toBeTrue();

expect(instance instanceof MyMixinA)
.withContext('should also be instance of mixin (a)')
.toBeTrue();
expect(instance instanceof MyMixinB)
.withContext('should also be instance of mixin (b)')
.toBeTrue();

// Instance check of via method in mixin
expect(instance.getThis() === instance)
.withContext('invalid instance from getThis()')
.toBeTrue();

// Debug
//console.log('invoked constructors', invoked);

// Constructors check
expect(invoked.length)
.withContext('Incorrect amount of constructors invoked')
.toEqual(3);
expect(invoked[0])
.withContext('Incorrect constructor invoked')
.toEqual('Mixin A');
expect(invoked[1])
.withContext('Incorrect constructor invoked')
.toEqual('Mixin B');
expect(invoked[2])
.withContext('Incorrect constructor invoked')
.toEqual('Class A');
});

it('mixin constructors are automatically invoked, even when class has no constructor defined', () => {

const invoked = [];
const MyMixinA = Mixin((superclass) => class extends superclass {
constructor() {
super();
invoked.push('Mixin A');
}

getThis() {
return this;
}
});

const MyMixinB = Mixin((superclass) => class extends superclass {
constructor() {
super();
invoked.push('Mixin B');
}
});

@mix(
MyMixinA,
MyMixinB
)
class A {
// NOTE: No constructor here, but mixin constructor(s) should still be invoked correctly
}

// -------------------------------------------------------------------------- //

const instance = new A();

// Debug
//console.log('invoked constructors', invoked);

// Constructors check
expect(invoked.length)
.withContext('Incorrect amount of constructors invoked')
.toEqual(2);
expect(invoked[0])
.withContext('Incorrect constructor invoked')
.toEqual('Mixin A');
expect(invoked[1])
.withContext('Incorrect constructor invoked')
.toEqual('Mixin B');
});

it('constructors invoked correctly, when extending parent with mixins', () => {

const invoked = [];

const MyMixinA = Mixin((superclass) => class extends superclass {
constructor() {
super();
console.log('Mixin A');
invoked.push('Mixin A');
}
});

const MyMixinB = Mixin((superclass) => class extends superclass {
constructor() {
super();
console.log('Mixin B');
invoked.push('Mixin B');
}
});

const MyMixinC = Mixin((superclass) => class extends superclass {
constructor() {
super();
console.log('Mixin C');
invoked.push('Mixin C');
}
});

const MyMixinD = Mixin((superclass) => class extends superclass {
constructor() {
super();
console.log('Mixin C');
invoked.push('Mixin D');
}
});

Expand All @@ -194,12 +319,7 @@ describe('@aedart/support/mixins', () => {
)
class A {
constructor() {
// TODO: Problem here... we cannot just call super(). This
// TODO: class is dynamically extended and its prototype set to inherit
// TODO: from a class it originally does not inherit from...
//super();

console.log('class A');
invoked.push('Class A');
}
}

Expand All @@ -210,14 +330,15 @@ describe('@aedart/support/mixins', () => {
class B extends A {
constructor() {
super();
console.log('class B');
invoked.push('Class B');
}
}

// -------------------------------------------------------------------------- //

const instance = new B();

// Inheritance check
expect(instance instanceof A)
.withContext('should be instance of class A')
.toBeTrue();
Expand All @@ -237,6 +358,32 @@ describe('@aedart/support/mixins', () => {
expect(instance instanceof MyMixinD)
.withContext('should also be instance of mixin (d)')
.toBeTrue();

// Debug
// console.log('invoked constructors', invoked);

// Constructors check
expect(invoked.length)
.withContext('Incorrect amount of constructors invoked')
.toEqual(6);
expect(invoked[0])
.withContext('Incorrect constructor invoked')
.toEqual('Mixin A');
expect(invoked[1])
.withContext('Incorrect constructor invoked')
.toEqual('Mixin B');
expect(invoked[2])
.withContext('Incorrect constructor invoked')
.toEqual('Class A');
expect(invoked[3])
.withContext('Incorrect constructor invoked')
.toEqual('Mixin C');
expect(invoked[4])
.withContext('Incorrect constructor invoked')
.toEqual('Mixin D');
expect(invoked[5])
.withContext('Incorrect constructor invoked')
.toEqual('Class B');
});
});
});

0 comments on commit 6786265

Please sign in to comment.