├── .gitattributes ├── deno.json ├── .npmrc ├── src ├── polytype-global.js ├── spec-runner.html.hbs └── polytype.d.ts.hbs ├── global └── package.json ├── .gitignore ├── test ├── init-spec.js ├── spec │ ├── ts-defs │ │ ├── 040.tstest │ │ ├── 140.tstest │ │ ├── 010.tstest │ │ ├── 020.tstest │ │ ├── 160.tstest │ │ ├── 150.tstest │ │ ├── 030.tstest │ │ ├── 180.tstest │ │ ├── 135.tstest │ │ ├── 170.tstest │ │ ├── 230.tstest │ │ ├── 220.tstest │ │ ├── 175.tstest │ │ ├── 200.tstest │ │ ├── 190.tstest │ │ ├── 210.tstest │ │ ├── 195.tstest │ │ ├── 120.tstest │ │ ├── 130.tstest │ │ ├── 070.tstest │ │ ├── 060.tstest │ │ ├── 080.tstest │ │ ├── 125.tstest │ │ ├── 100.tstest │ │ ├── 050.tstest │ │ ├── 090.tstest │ │ └── 110.tstest │ ├── common │ │ ├── clustered-prototype.spec.js │ │ ├── define-globally.spec.js │ │ ├── super-new-target.spec.js │ │ ├── instanceof.spec.js │ │ ├── function-prototype-bind.spec.js │ │ ├── object-prototype-is-prototype-of.spec.js │ │ ├── is-prototype-of-call.spec.js │ │ ├── get-prototype-list-of.spec.js │ │ ├── classes.spec.js │ │ ├── clustered-constructor.spec.js │ │ ├── symbol-has-instance.spec.js │ │ ├── super-class.spec.js │ │ ├── bind-call.spec.js │ │ └── polytype.spec.js │ └── ts-defs.spec.mjs ├── reimport.mjs ├── patch-cov-source.mjs ├── browser-spec-runner.js ├── deno-spec-runner.mjs ├── node-spec-runner.mjs ├── eslint-plugin-tstest.mjs ├── serve.mjs └── spec-helper.js ├── tsconfig.json ├── license.txt ├── .github └── workflows │ └── ci.yml ├── package.json ├── example ├── ColoredCircle.js └── ColoredCircle.ts ├── changelog.md └── gulpfile.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false 3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | install-links=false 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /src/polytype-global.js: -------------------------------------------------------------------------------- 1 | import { defineGlobally } from './polytype-esm'; 2 | 3 | defineGlobally(); 4 | -------------------------------------------------------------------------------- /global/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "../lib/polytype.js", 3 | "types": "../lib/polytype-global.d.ts" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.git* 3 | !/.npmrc 4 | /coverage 5 | /lib 6 | /node_modules 7 | /npm-debug.log 8 | /test/spec-runner.html 9 | -------------------------------------------------------------------------------- /test/init-spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global loadPolytype */ 3 | 4 | 'use strict'; 5 | 6 | before(loadPolytype); 7 | -------------------------------------------------------------------------------- /test/spec/ts-defs/040.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Object.getPrototypeListOf', 4 | polytypeMode: 'global', 5 | } 6 | */ 7 | 8 | void Object.getPrototypeListOf; 9 | -------------------------------------------------------------------------------- /test/spec/ts-defs/140.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'classes without arguments', 4 | expectedMessage: 'Expected at least 1 arguments, but got 0.', 5 | } 6 | */ 7 | 8 | classes(); 9 | -------------------------------------------------------------------------------- /test/spec/ts-defs/010.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'SuperConstructorInvokeInfo (module version)', 4 | polytypeMode: 'module', 5 | } 6 | */ 7 | 8 | import type { SuperConstructorInvokeInfo } from 'polytype'; 9 | -------------------------------------------------------------------------------- /test/spec/ts-defs/020.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'SuperConstructorInvokeInfo (global version)', 4 | polytypeMode: 'global', 5 | } 6 | */ 7 | 8 | import type { SuperConstructorInvokeInfo } from 'polytype/global'; 9 | -------------------------------------------------------------------------------- /test/spec/ts-defs/160.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'classes with a null argument', 4 | expectedMessage: 5 | 'Argument of type \'null\' is not assignable to parameter of type \'SuperConstructor\'.', 6 | } 7 | */ 8 | 9 | classes(null); 10 | -------------------------------------------------------------------------------- /test/spec/ts-defs/150.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'classes with an undefined argument', 4 | expectedMessage: 5 | 'Argument of type \'undefined\' is not assignable to parameter of type \'SuperConstructor\'.', 6 | } 7 | */ 8 | 9 | classes(undefined); 10 | -------------------------------------------------------------------------------- /test/spec/ts-defs/030.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'classes global is read-only', 4 | polytypeMode: 'global', 5 | expectedMessage: 'Cannot assign to \'classes\' because it is a function.', 6 | } 7 | */ 8 | 9 | classes = { } as typeof classes; 10 | -------------------------------------------------------------------------------- /test/spec/ts-defs/180.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Assignment to property \'prototype\' of a clustered constuctor', 4 | expectedMessage: 'Cannot assign to \'prototype\' because it is a read-only property.', 5 | } 6 | */ 7 | 8 | classes(Object).prototype = null; 9 | -------------------------------------------------------------------------------- /test/reimport.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-env shared-node-browser */ 2 | 3 | let counter = 0; 4 | 5 | export default function (relativeURL) 6 | { 7 | const url = new URL(relativeURL, import.meta.url); 8 | url.search = `${++counter}`; 9 | const promise = import(url); 10 | return promise; 11 | } 12 | -------------------------------------------------------------------------------- /test/spec/ts-defs/135.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'classes with constructors of object types', 4 | } 5 | */ 6 | 7 | declare const A: new () => Boolean; 8 | declare const B: new () => number[]; 9 | declare const C: new () => new () => void; 10 | declare const D: new () => never; 11 | classes(A, B, C, D); 12 | -------------------------------------------------------------------------------- /test/spec/ts-defs/170.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'classes with a non-constructor function argument', 4 | expectedMessage: 5 | 'Argument of type \'() => {}\' is not assignable to parameter of type \'SuperConstructor\'.\n' + 6 | ' Type \'() => {}\' provides no match for the signature \'new (...args: any): object\'.', 7 | } 8 | */ 9 | 10 | classes((): { } => ({ })); 11 | -------------------------------------------------------------------------------- /test/spec/ts-defs/230.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Superclass selector in static context out of class body', 4 | expectedMessage: 5 | 'Property \'class\' is protected and only accessible within class ' + 6 | '\'SuperConstructorSelector\' and its subclasses.', 7 | } 8 | */ 9 | 10 | class Test extends classes(Object) 11 | { } 12 | 13 | Test.class(Object).create(null); 14 | -------------------------------------------------------------------------------- /test/spec/ts-defs/220.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Superclass selector in nonstatic context out of class body', 4 | expectedMessage: 5 | 'Property \'class\' is protected and only accessible within class ' + 6 | '\'SuperPrototypeSelector\' and its subclasses.', 7 | } 8 | */ 9 | 10 | class Test extends classes(Object) 11 | { } 12 | 13 | new Test().class(Object).valueOf(); 14 | -------------------------------------------------------------------------------- /test/spec/ts-defs/175.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'classes with a constructor of a primitive type', 4 | expectedMessage: 5 | 'Argument of type \'new () => string\' is not assignable to parameter of type ' + 6 | '\'SuperConstructor\'.\n' + 7 | ' Type \'string\' is not assignable to type \'object\'.', 8 | } 9 | */ 10 | 11 | declare const Foo: new () => string; 12 | classes(Foo); 13 | -------------------------------------------------------------------------------- /test/spec/ts-defs/200.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Hidden static overload', 4 | expectedMessage: 'Expected 1 arguments, but got 0.', 5 | } 6 | */ 7 | 8 | class A 9 | { 10 | public static x(x: any): void 11 | { } 12 | } 13 | 14 | class B 15 | { 16 | public static x(): void 17 | { } 18 | } 19 | 20 | class C extends classes(A, B) 21 | { } 22 | 23 | C.x(); 24 | -------------------------------------------------------------------------------- /test/spec/ts-defs/190.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Property \'prototype\' of a clustered constuctor with an interface as superclass', 4 | expectedMessage: 5 | 'Property \'prototype\' is missing in type \'{}\' but required in type \'Test\'.', 6 | } 7 | */ 8 | 9 | interface Test 10 | { 11 | readonly prototype: { }; 12 | new(): Test; 13 | } 14 | 15 | const testPrototype: Test = classes({ } as Test).prototype; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "exactOptionalPropertyTypes": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "module": "Node16", 6 | "moduleResolution": "Node16", 7 | "noUncheckedSideEffectImports": true, 8 | "strict": true, 9 | "target": "ES2020", 10 | "verbatimModuleSyntax": true 11 | }, 12 | "include": [ 13 | "example", 14 | "lib", 15 | "test" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /test/spec/ts-defs/210.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Union superclass', 4 | expectedMessage: 5 | 'Base constructor return type \'ClusteredPrototype<[typeof A | typeof B]>\' is not an object ' + 6 | 'type or intersection of object types with statically known members.', 7 | } 8 | */ 9 | 10 | class A 11 | { } 12 | 13 | class B 14 | { } 15 | 16 | void 17 | ( 18 | (C: typeof A | typeof B): new () => unknown => 19 | class extends classes(C) 20 | { } 21 | ); 22 | -------------------------------------------------------------------------------- /test/spec/ts-defs/195.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 4 | 'Property \'prototype\' of a clustered constuctor with a parameterized superclass', 5 | expectedMessage: 'Type \'number\' is not assignable to type \'string\'.', 6 | } 7 | */ 8 | 9 | class Test 10 | { 11 | public value?: T; 12 | } 13 | 14 | new class extends classes(Test) { }().value = 56; 15 | 16 | void 17 | function (value: { 1: 1; 2: 2; 3: 3; }): void 18 | { 19 | new class extends classes(Test<{ 1: 1; 2: 2; }>, Test<{ 2: 2; 3: 3; }>) { }().value = value; 20 | }; 21 | -------------------------------------------------------------------------------- /test/spec/ts-defs/120.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'super.class in nonstatic context with indirect superclass', 4 | expectedMessage: 5 | 'Argument of type \'typeof A\' is not assignable to parameter of type \'typeof B\'.\n' + 6 | ' Property \'b\' is missing in type \'A\' but required in type \'B\'.', 7 | } 8 | */ 9 | 10 | class A 11 | { } 12 | 13 | class B extends A 14 | { 15 | public b(): void 16 | { } 17 | } 18 | 19 | void 20 | class extends classes(B) 21 | { 22 | public c(): void 23 | { 24 | super.class(A); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /test/spec/ts-defs/130.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'super.class in static context with indirect superclass', 4 | expectedMessage: 5 | 'Argument of type \'typeof A\' is not assignable to parameter of type \'typeof B\'.\n' + 6 | ' Property \'b\' is missing in type \'typeof A\' but required in type \'typeof B\'.', 7 | } 8 | */ 9 | 10 | class A 11 | { } 12 | 13 | class B extends A 14 | { 15 | public static b(): void 16 | { } 17 | } 18 | 19 | void 20 | class extends classes(B) 21 | { 22 | public static c(): void 23 | { 24 | super.class(A); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /test/spec/ts-defs/070.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Merging of nonstatic members', 4 | } 5 | */ 6 | 7 | class A 8 | { 9 | public m1 = 'a'; 10 | public m2 = 1; 11 | } 12 | 13 | class B 14 | { 15 | public m1 = 1; 16 | public m2 = 2; 17 | } 18 | 19 | class C extends classes(A, B) { } 20 | 21 | const c = new C(); 22 | 23 | const test1: never = c.m1; // c.m1 should be a never 24 | 25 | // @ts-expect-error 26 | const test2a: typeof c.m2 = undefined as unknown; // c.m2 should not be any or unknown 27 | const test2b: number = c.m2; 28 | const test2c: typeof c.m2 = test2b; // c.m2 should be a number 29 | -------------------------------------------------------------------------------- /test/spec/ts-defs/060.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Abstract class inheritance', 4 | } 5 | */ 6 | 7 | abstract class A 8 | { 9 | public abstract foo(): number; 10 | } 11 | 12 | abstract class B 13 | { 14 | public foo(): string 15 | { 16 | return 'foo'; 17 | } 18 | public abstract bar(arg: number): void; 19 | } 20 | 21 | abstract class C extends B 22 | { 23 | public abstract get baz(): boolean; 24 | } 25 | 26 | class D extends classes(A, C) 27 | { 28 | public baz = true; 29 | 30 | public bar(): void 31 | { } 32 | } 33 | 34 | // @ts-expect-error 35 | class E extends A 36 | { } 37 | 38 | const a: A = new D(); 39 | const b: B = new D(); 40 | const c: C = new D(); 41 | -------------------------------------------------------------------------------- /test/spec/ts-defs/080.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Merging of static members', 4 | } 5 | */ 6 | 7 | class A 8 | { 9 | public static sm1 = 'a'; 10 | public static sm2 = 1; 11 | } 12 | 13 | class B 14 | { 15 | public static sm1 = 1; 16 | public static sm2 = 2; 17 | } 18 | 19 | class C extends classes(A, B) { } 20 | 21 | // @ts-expect-error 22 | const test1a: typeof C.sm1 = undefined as unknown; // C.sm1 should not be any or unknown 23 | const test1b: string = C.sm1; 24 | const test1c: typeof C.sm1 = test1b; // C.sm1 should be a string 25 | 26 | // @ts-expect-error 27 | const test2a: typeof C.sm2 = undefined as unknown; // C.sm2 should not be any or unknown 28 | const test2b: number = C.sm2; 29 | const test2c: typeof C.sm2 = test2b; // C.sm2 should be a number 30 | -------------------------------------------------------------------------------- /test/spec/ts-defs/125.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'super.class in nonstatic context with a parameterized superclass', 4 | expectedMessage: 'Type \'{}\' is not assignable to type \'bigint\'.', 5 | } 6 | */ 7 | 8 | class Test 9 | { 10 | public value?: T; 11 | } 12 | 13 | void 14 | class extends classes(Test) 15 | { 16 | public test(): void 17 | { 18 | super.class(Test).value = { }; 19 | } 20 | }; 21 | 22 | void 23 | class extends classes(Test, Test) 24 | { 25 | public test(): void 26 | { 27 | super.class(Test).value = 'foo'; 28 | super.class(Test).value = 42; 29 | // @ts-expect-error 30 | super.class(Test).value = true; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright Francesco Trotta (https://github.com/fasttime) 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /test/patch-cov-source.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | import { createRequire } from 'node:module'; 4 | 5 | const IGNORED_LINE = 6 | ' throw Error(\'Polytype cannot be transpiled to ES5 or earlier code.\');\n'; 7 | 8 | const require = createRequire(import.meta.url); 9 | const c8jsRequire = createRequire(require.resolve('c8js')); 10 | const c8Require = createRequire(c8jsRequire.resolve('c8')); 11 | const { prototype: covSourcePrototype } = c8Require('v8-to-istanbul/lib/source'); 12 | const { _parseIgnore } = covSourcePrototype; 13 | covSourcePrototype._parseIgnore = 14 | function (lineStr) 15 | { 16 | let ignoreToken = _parseIgnore.call(this, lineStr); 17 | if (ignoreToken) 18 | return ignoreToken; 19 | if (lineStr === IGNORED_LINE) 20 | { 21 | ignoreToken = { count: 0 }; 22 | return ignoreToken; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /test/browser-spec-runner.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | /* global MochaBar mocha */ 3 | 4 | 'use strict'; 5 | 6 | mocha.setup({ checkLeaks: true, reporter: MochaBar, ui: 'bdd' }); 7 | addEventListener 8 | ( 9 | 'DOMContentLoaded', 10 | () => 11 | { 12 | mocha.run(); 13 | const extnameField = document.getElementById('extname'); 14 | const urlParams = new URLSearchParams(location.search); 15 | const extname = urlParams.get('extname'); 16 | extnameField.value = extname; 17 | if (!extnameField.value) 18 | extnameField.options[0].selected = true; 19 | extnameField.addEventListener 20 | ( 21 | 'change', 22 | () => 23 | { 24 | const extname = extnameField.value; 25 | const url = new URL(location); 26 | url.searchParams.set('extname', extname); 27 | location.assign(url); 28 | }, 29 | ); 30 | }, 31 | ); 32 | -------------------------------------------------------------------------------- /test/spec/ts-defs/100.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Superconstructor arguments', 4 | } 5 | */ 6 | 7 | class A 8 | { 9 | public constructor(n: number, s?: string) 10 | { 11 | void null; 12 | } 13 | } 14 | 15 | abstract class B 16 | { 17 | public constructor(o: object) 18 | { 19 | void null; 20 | } 21 | } 22 | 23 | void 24 | class extends classes(A, B) 25 | { 26 | public constructor(args?: ConstructorParameters) 27 | { 28 | super(); 29 | super([1]); 30 | super([1, 'A'], [{ }]); 31 | const ASCII = { super: A, arguments: [1, 'A'] } as const; 32 | const BSCII = { super: B, arguments: [{ }] } as const; 33 | super(BSCII, ASCII); 34 | super(BSCII); 35 | super({ super: A }); 36 | super({ super: A, arguments: args }); 37 | // @ts-expect-error 38 | super([1, 'A'], BSCII); 39 | // @ts-expect-error 40 | super(['A']); 41 | // @ts-expect-error 42 | super({ super: A, arguments: ['A'] }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/spec-runner.html.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{#each specs}} 14 | 15 | {{/each}} 16 | 25 | Polytype Library Test Suite 26 |

Polytype Library Test Suite

27 |
28 | 34 |
35 | -------------------------------------------------------------------------------- /test/spec/ts-defs/050.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Ordinary multiple inheritance', 4 | } 5 | */ 6 | 7 | class A 8 | { 9 | public static sa(): void 10 | { } 11 | 12 | protected static _sa(): void 13 | { } 14 | 15 | public a(): void 16 | { } 17 | 18 | protected _a(): void 19 | { } 20 | } 21 | 22 | class B 23 | { 24 | public static sb(): void 25 | { } 26 | 27 | protected static _sb(): void 28 | { } 29 | 30 | public b(): void 31 | { } 32 | 33 | protected _b(): void 34 | { } 35 | } 36 | 37 | class C extends classes(A, B) 38 | { 39 | public static sc(): void 40 | { 41 | C.sa(); 42 | C.sb(); 43 | this.sa(); 44 | this.sb(); 45 | super.sa(); 46 | super.sb(); 47 | super.class(A).sa(); 48 | super.class(B).sb(); 49 | } 50 | 51 | protected static _sc(): void 52 | { 53 | super.class(A)._sa(); 54 | super.class(B)._sb(); 55 | } 56 | 57 | public c(): void 58 | { 59 | this.a(); 60 | this.b(); 61 | super.a(); 62 | super.b(); 63 | super.class(A).a(); 64 | super.class(B).b(); 65 | } 66 | 67 | protected _c(): void 68 | { 69 | this._a(); 70 | this._b(); 71 | super._a(); 72 | super._b(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/spec/ts-defs/090.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Assignability of constructors and prototypes', 4 | } 5 | */ 6 | 7 | class T1 { public m1 = 1 as const; } 8 | 9 | abstract class T2 { public m2 = 2 as const; } 10 | 11 | class T3 { public m3 = 3 as const; } 12 | 13 | class T4 { public m4 = 4 as const; } 14 | 15 | class T5 { public m5 = 5 as const; } 16 | 17 | class T6 { public m6 = 6 as const; } 18 | 19 | class T7 { public m7 = 7 as const; } 20 | 21 | class T8 { public m8 = 8 as const; } 22 | 23 | class T9 { public m9 = 9 as const; } 24 | 25 | class T10 { public m10 = 10 as const; } 26 | 27 | class T11 { public m11 = 11 as const; } 28 | 29 | class U extends classes(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) { } 30 | 31 | void (U.prototype as T1); 32 | void (U.prototype as T2); 33 | void (U.prototype as T3); 34 | void (U.prototype as T4); 35 | void (U.prototype as T5); 36 | void (U.prototype as T6); 37 | void (U.prototype as T7); 38 | void (U.prototype as T8); 39 | void (U.prototype as T9); 40 | void (U.prototype as T10); 41 | void (U.prototype as T11); 42 | void (U as typeof T1); 43 | void (U as typeof T2); 44 | void (U as typeof T3); 45 | void (U as typeof T4); 46 | void (U as typeof T5); 47 | void (U as typeof T6); 48 | void (U as typeof T7); 49 | void (U as typeof T8); 50 | void (U as typeof T9); 51 | void (U as typeof T10); 52 | void (U as typeof T11); 53 | -------------------------------------------------------------------------------- /test/deno-spec-runner.mjs: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | 3 | Run with: 4 | 5 | deno run --allow-env --allow-read test/deno-spec-runner.mjs 6 | 7 | ***************************************************************************************************/ 8 | 9 | /* eslint-env browser, mocha */ 10 | /* global Deno */ 11 | 12 | import { expandGlob } from 'https://deno.land/std/fs/expand_glob.ts'; 13 | import { dirname, fromFileUrl, toFileUrl } from 'https://deno.land/std/path/mod.ts'; 14 | import * as chai from 'https://esm.sh/chai'; 15 | import 'https://esm.sh/mocha/mocha.js'; 16 | 17 | globalThis.chai = chai; 18 | await import('./spec-helper.js'); 19 | 20 | // Browser based Mocha expects `window.location` to exist. 21 | Object.defineProperty(globalThis, 'window', { value: globalThis, enumerable: true }); 22 | window.location = new URL('about:blank'); 23 | 24 | mocha.setup({ checkLeaks: true, reporter: 'spec', ui: 'bdd' }); 25 | await import('./init-spec.js'); 26 | { 27 | const __dirname = dirname(fromFileUrl(import.meta.url)); 28 | const asyncFiles = await expandGlob('spec/common/**/*.spec.{js,mjs}', { root: __dirname }); 29 | for await (const { path } of asyncFiles) 30 | { 31 | const fileUrl = toFileUrl(path); 32 | await import(fileUrl); 33 | } 34 | } 35 | mocha.run 36 | ( 37 | failures => 38 | { 39 | if (failures) 40 | Deno.exit(1); 41 | }, 42 | ); 43 | -------------------------------------------------------------------------------- /test/spec/common/clustered-prototype.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert classes */ 3 | 4 | 'use strict'; 5 | 6 | describe 7 | ( 8 | 'Clustered prototype [classes(...?).prototype]', 9 | () => 10 | { 11 | it 12 | ( 13 | 'has unsettable prototype', 14 | () => 15 | assert.throwsTypeError(() => Object.setPrototypeOf(classes(Function()).prototype, { })), 16 | ); 17 | 18 | it 19 | ( 20 | 'has expected own properties', 21 | () => 22 | { 23 | const constructor = classes(Function()); 24 | const { prototype } = constructor; 25 | assert.hasOwnPropertyDescriptors 26 | ( 27 | prototype, 28 | { 29 | class: 30 | { 31 | configurable: false, 32 | enumerable: false, 33 | value: prototype.class, 34 | writable: false, 35 | }, 36 | constructor: 37 | { 38 | configurable: true, 39 | enumerable: false, 40 | value: constructor, 41 | writable: true, 42 | }, 43 | }, 44 | ); 45 | }, 46 | ); 47 | }, 48 | ); 49 | -------------------------------------------------------------------------------- /test/spec/ts-defs/110.tstest: -------------------------------------------------------------------------------- 1 | /*!TESTDATA 2 | { 3 | title: 'Superconstructor argument referencing indirect superclass', 4 | expectedMessage: 5 | [ 6 | // TypeScript < 5.3 7 | 'No overload matches this call.\n' + 8 | ' Overload 1 of 2, \'(args_0?: readonly [] | undefined): ClusteredPrototype<[typeof ' + 9 | 'B]>\', gave the following error.\n' + 10 | ' Argument of type \'{ super: typeof A; }\' is not assignable to parameter of type ' + 11 | '\'readonly []\'.\n' + 12 | ' Object literal may only specify known properties, and \'super\' does not exist in ' + 13 | 'type \'readonly []\'.\n' + 14 | ' Overload 2 of 2, \'(...args: Readonly>[]): ' + 15 | 'ClusteredPrototype<[typeof B]>\', gave the following error.\n' + 16 | ' Type \'typeof A\' is not assignable to type \'typeof B\'.\n' + 17 | ' Property \'b\' is missing in type \'A\' but required in type \'B\'.', 18 | 19 | // TypeScript >= 5.3 20 | 'No overload matches this call.\n' + 21 | ' Overload 1 of 2, \'(args_0?: readonly [] | undefined): ClusteredPrototype<[typeof ' + 22 | 'B]>\', gave the following error.\n' + 23 | ' Object literal may only specify known properties, and \'super\' does not exist in ' + 24 | 'type \'readonly []\'.\n' + 25 | ' Overload 2 of 2, \'(...args: Readonly>[]): ' + 26 | 'ClusteredPrototype<[typeof B]>\', gave the following error.\n' + 27 | ' Type \'typeof A\' is not assignable to type \'typeof B\'.\n' + 28 | ' Property \'b\' is missing in type \'A\' but required in type \'B\'.', 29 | ], 30 | } 31 | */ 32 | 33 | class A 34 | { } 35 | 36 | class B extends A 37 | { 38 | public b(): void 39 | { } 40 | } 41 | 42 | void 43 | class extends classes(B) 44 | { 45 | public constructor() 46 | { 47 | super({ super: A }); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /test/node-spec-runner.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-env node */ 4 | 5 | (async () => { 6 | function addMissingFlag(flag) 7 | { 8 | const regExp = RegExp(`${flag}(?![^=])`); 9 | const flagFound = execArgv.some(arg => regExp.test(arg)); 10 | if (!flagFound) 11 | childExecArgv.push(flag); 12 | } 13 | 14 | const { execArgv } = process; 15 | const childExecArgv = [...execArgv]; 16 | addMissingFlag('--experimental-vm-modules'); 17 | if (childExecArgv.length > execArgv.length) 18 | { 19 | const { fork } = await import('node:child_process'); 20 | 21 | const [, modulePath, ...args] = process.argv; 22 | addMissingFlag('--no-warnings'); 23 | const childProcess = fork(modulePath, args, { execArgv: childExecArgv }); 24 | childProcess.on 25 | ( 26 | 'exit', 27 | (code, signal) => 28 | { 29 | process.exitCode = code != null ? code : 128 + signal; 30 | }, 31 | ); 32 | return; 33 | } 34 | const promises = 35 | ['./spec-helper.js', 'node:inspector', 'mocha', 'node:url', 'glob'] 36 | .map(specifier => import(specifier)); 37 | const [, { url }, { default: Mocha }, { fileURLToPath }, { glob }] = 38 | await Promise.all(promises); 39 | { 40 | const inspectorUrl = url(); 41 | if (inspectorUrl) 42 | Mocha.Runnable.prototype.timeout = (...args) => args.length ? undefined : 0; 43 | } 44 | const mocha = new Mocha({ checkLeaks: true }); 45 | const currentURL = import.meta.url; 46 | mocha.addFile(fileURLToPath(new URL('./init-spec.js', currentURL))); 47 | { 48 | const __dirname = fileURLToPath(new URL('.', currentURL)); 49 | const filenames = await glob('spec/**/*.spec.{js,mjs}', { absolute: true, cwd: __dirname }); 50 | for (const filename of filenames) 51 | mocha.addFile(filename); 52 | } 53 | await mocha.loadFilesAsync(); 54 | mocha.run 55 | ( 56 | failures => 57 | { 58 | if (failures) 59 | process.exitCode = 1; 60 | }, 61 | ); 62 | })(); 63 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: ['**'] 6 | tags-ignore: ['**'] 7 | pull_request: 8 | branches: ['**'] 9 | 10 | jobs: 11 | 12 | build: 13 | 14 | name: Build 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v5 20 | - uses: actions/setup-node@v6 21 | with: 22 | node-version: '24' 23 | - run: | 24 | npm install 25 | gulp make-ts-defs 26 | gulp lint 27 | gulp bundle:cjs bundle:esm bundle:global 28 | - uses: actions/upload-artifact@v4 29 | with: 30 | name: lib 31 | path: lib 32 | 33 | node-test: 34 | 35 | name: Test (Node.js ${{ matrix.node-version }}) 36 | 37 | needs: build 38 | 39 | runs-on: ubuntu-latest 40 | 41 | strategy: 42 | matrix: 43 | node-version: ['16.0.0', '16', '18', '20', '22', '24', '25'] 44 | 45 | steps: 46 | - uses: actions/checkout@v5 47 | - uses: actions/setup-node@v6 48 | with: 49 | node-version: ${{ matrix.node-version }} 50 | - run: npm uninstall @origin-1/eslint-config 51 | - uses: actions/download-artifact@v5 52 | with: 53 | name: lib 54 | path: lib 55 | - run: | 56 | npm test -- .cjs 57 | npm test -- .mjs 58 | npm test -- .min.mjs 59 | npm test -- .js 60 | npm test -- .min.js 61 | 62 | deno-test: 63 | 64 | name: Test (Deno ${{ matrix.deno-version }}) 65 | 66 | needs: build 67 | 68 | runs-on: ubuntu-latest 69 | 70 | strategy: 71 | matrix: 72 | deno-version: [v1.24, v1.x, v2.x] 73 | 74 | steps: 75 | - uses: actions/checkout@v5 76 | - uses: denoland/setup-deno@v2 77 | with: 78 | deno-version: ${{ matrix.deno-version }} 79 | - uses: actions/download-artifact@v5 80 | with: 81 | name: lib 82 | path: lib 83 | - run: | 84 | deno run --allow-env --allow-read test/deno-spec-runner.mjs .mjs 85 | deno run --allow-env --allow-read test/deno-spec-runner.mjs .min.mjs 86 | deno run --allow-env --allow-read test/deno-spec-runner.mjs .js 87 | deno run --allow-env --allow-read test/deno-spec-runner.mjs .min.js 88 | -------------------------------------------------------------------------------- /test/spec/common/define-globally.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert backupGlobals loadPolytype maybeDescribe polytypeMode */ 3 | 4 | 'use strict'; 5 | 6 | maybeDescribe 7 | ( 8 | polytypeMode === 'module', 9 | 'defineGlobally', 10 | () => 11 | { 12 | let defineGlobally; 13 | 14 | before 15 | (async () => { defineGlobally = await loadPolytype(); }); 16 | 17 | after 18 | (() => { defineGlobally = null; }); 19 | 20 | backupGlobals(); 21 | 22 | describe 23 | ( 24 | 'with a falsy argument', 25 | () => 26 | { 27 | it 28 | ( 29 | 'returns true', 30 | () => 31 | { 32 | delete globalThis.classes; 33 | assert.strictEqual(defineGlobally(), true); 34 | assert.isFunction(globalThis.classes); 35 | assert.isFunction(Object.getPrototypeListOf); 36 | }, 37 | ); 38 | 39 | it 40 | ( 41 | 'returns false', 42 | () => 43 | { 44 | assert.strictEqual(defineGlobally(), false); 45 | }, 46 | ); 47 | }, 48 | ); 49 | 50 | describe 51 | ( 52 | 'with a truthy argument', 53 | () => 54 | { 55 | it 56 | ( 57 | 'returns true', 58 | () => 59 | { 60 | assert.strictEqual(defineGlobally(true), true); 61 | assert.isNotFunction(globalThis.classes); 62 | assert.isNotFunction(Object.getPrototypeListOf); 63 | }, 64 | ); 65 | 66 | it 67 | ( 68 | 'returns false', 69 | () => 70 | { 71 | delete globalThis.classes; 72 | assert.strictEqual(defineGlobally(true), false); 73 | }, 74 | ); 75 | }, 76 | ); 77 | }, 78 | ); 79 | -------------------------------------------------------------------------------- /test/spec/common/super-new-target.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert classes */ 3 | 4 | 'use strict'; 5 | 6 | describe 7 | ( 8 | 'new.target in superconstructor', 9 | () => 10 | { 11 | let _superNewTarget; 12 | let _SubClass; 13 | 14 | beforeEach 15 | ( 16 | () => 17 | { 18 | class SuperClass 19 | { 20 | constructor() 21 | { 22 | _superNewTarget = new.target; 23 | } 24 | } 25 | 26 | class InterClass extends classes(SuperClass) 27 | { } 28 | 29 | _SubClass = 30 | class extends classes(InterClass) 31 | { }; 32 | new _SubClass(); 33 | return _superNewTarget; 34 | }, 35 | ); 36 | 37 | afterEach 38 | ( 39 | () => 40 | { 41 | _superNewTarget = _SubClass = undefined; 42 | }, 43 | ); 44 | 45 | it 46 | ( 47 | 'has expected own properties', 48 | () => 49 | assert.hasOwnPropertyDescriptors 50 | ( 51 | _superNewTarget, 52 | { 53 | prototype: 54 | { 55 | configurable: false, 56 | enumerable: false, 57 | value: _superNewTarget.prototype, 58 | writable: false, 59 | }, 60 | }, 61 | ), 62 | ); 63 | 64 | it 65 | ( 66 | 'has expected property \'prototype\'', 67 | () => assert.instanceOf(_superNewTarget.prototype, _SubClass), 68 | ); 69 | 70 | it 71 | ( 72 | 'has expected prototype', 73 | () => assert(_SubClass.isPrototypeOf(_superNewTarget)), 74 | ); 75 | 76 | it('is not extensible', () => assert.isNotExtensible(_superNewTarget)); 77 | 78 | it 79 | ( 80 | 'cannot be called', 81 | () => 82 | { 83 | assert.throwsTypeError(_superNewTarget, 'Operation not supported'); 84 | assert.throwsTypeError(() => new _superNewTarget(), 'Operation not supported'); 85 | }, 86 | ); 87 | }, 88 | ); 89 | -------------------------------------------------------------------------------- /test/eslint-plugin-tstest.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | export function getImportStatement(polytypeMode) 4 | { 5 | switch (polytypeMode) 6 | { 7 | case 'global': 8 | return 'import \'polytype/global\';'; 9 | case 'module': 10 | return 'import { classes } from \'polytype\';'; 11 | default: 12 | throw Error(`Unknown mode ${polytypeMode}`); 13 | } 14 | } 15 | 16 | export function getTestCase(code) 17 | { 18 | const match = 19 | code.match(/(?<=^|\u2028|\u2029)[^\S\r\n\u2028\u2029]?\/\*!TESTDATA\b(?.*?)\*\//msu); 20 | if (!match) 21 | return; 22 | const matchIndex = match.index; 23 | const before = code.slice(0, matchIndex); 24 | const after = code.slice(matchIndex + match[0].length); 25 | const { testData } = match.groups; 26 | const functionBody = `return(${testData})`; 27 | const testCase = Function(functionBody)(); 28 | testCase.testData = testData; 29 | testCase.before = before; 30 | testCase.after = after; 31 | return testCase; 32 | } 33 | 34 | function postprocess(messageLists, filename) 35 | { 36 | const [messages, testDataMessages] = messageLists; 37 | if (testDataMessages) 38 | { 39 | const testCase = fileNameToTestCaseMap.get(filename); 40 | fileNameToTestCaseMap.delete(filename); 41 | const rowDiff = (testCase.before.match(/\r\n|[\r\n\u2028\u2029]/gu)?.length || 0) - 1; 42 | for (const message of testDataMessages) 43 | { 44 | message.filename = filename; 45 | const { line, endLine } = message; 46 | message.line = line + rowDiff; 47 | if (endLine != null) 48 | message.endLine = endLine + rowDiff; 49 | } 50 | messages.push(...testDataMessages); 51 | } 52 | return messages; 53 | } 54 | 55 | function preprocess(text, filename) 56 | { 57 | const testCase = getTestCase(text); 58 | if (!testCase) 59 | return [{ text, filename: '/..' }]; 60 | fileNameToTestCaseMap.set(filename, testCase); 61 | const polytypeMode = testCase.polytypeMode ?? 'global'; 62 | const importStatement = getImportStatement(polytypeMode); 63 | const testDataText = 64 | `void\n${testCase.testData}\n; // eslint-disable-line @stylistic/semi-style\n`; 65 | const processedCode = processTestCase(testCase, importStatement); 66 | const returnValue = 67 | [{ text: processedCode, filename: '/..' }, { text: testDataText, filename: '/test-data.mjs' }]; 68 | return returnValue; 69 | } 70 | 71 | export function processTestCase({ testData, before, after }, importStatement) 72 | { 73 | const processedCode = `${before}${importStatement}/* TESTDATA${testData}*/${after}`; 74 | return processedCode; 75 | } 76 | 77 | const fileNameToTestCaseMap = new Map(); 78 | 79 | export const processor = { preprocess, postprocess }; 80 | -------------------------------------------------------------------------------- /test/serve.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-env node */ 4 | 5 | import { createReadStream } from 'node:fs'; 6 | import { createServer } from 'node:http'; 7 | import { networkInterfaces } from 'node:os'; 8 | import { extname, join } from 'node:path'; 9 | import { fileURLToPath } from 'node:url'; 10 | import ansiColors from 'ansi-colors'; 11 | 12 | const baseDir = fileURLToPath(new URL('..', import.meta.url)); 13 | const mimeTypes = 14 | { 15 | '.css': 'text/css', 16 | '.html': 'text/html', 17 | '.js': 'application/javascript', 18 | '.json': 'application/json', 19 | '.mjs': 'application/javascript', 20 | }; 21 | const port = 8080; 22 | createServer 23 | ( 24 | ({ url }, response) => 25 | { 26 | const { pathname } = new URL(url, 'file:'); 27 | if (pathname === '/favicon.ico') 28 | { 29 | const headers = { 'Content-Type': 'image/x-icon' }; 30 | response.writeHead(204, headers); 31 | response.end(); 32 | return; 33 | } 34 | const fullPath = join(baseDir, pathname); 35 | const stream = createReadStream(fullPath); 36 | stream.on 37 | ( 38 | 'open', 39 | () => 40 | { 41 | const headers = { }; 42 | { 43 | const ext = extname(fullPath); 44 | if (mimeTypes.hasOwnProperty(ext)) 45 | headers['Content-Type'] = mimeTypes[ext]; 46 | } 47 | response.writeHead(200, headers); 48 | stream.pipe(response); 49 | }, 50 | ); 51 | stream.on 52 | ( 53 | 'error', 54 | () => 55 | { 56 | if (!response.headersSent) 57 | response.writeHead(404); 58 | response.end(); 59 | }, 60 | ); 61 | }, 62 | ) 63 | .listen(port); 64 | 65 | { 66 | const ip = getIP(); 67 | if (ip) 68 | { 69 | const { blue, bold } = ansiColors; 70 | const baseUrl = `http://${ip}:${port}`; 71 | console.log(`\n${bold('Spec Runner URL')}\n${blue(`${baseUrl}/test/spec-runner.html`)}\n`); 72 | } 73 | } 74 | 75 | function getIP() 76 | { 77 | let ip; 78 | const networkInterfaceList = Object.values(networkInterfaces()); 79 | for (const networkInterface of networkInterfaceList) 80 | { 81 | for (const assignedNetworkAddress of networkInterface) 82 | { 83 | if (!assignedNetworkAddress.internal) 84 | { 85 | let { address } = assignedNetworkAddress; 86 | const { family } = assignedNetworkAddress; 87 | // For IPv4, family is 4 in Node.js 18.0-18.3. 88 | if (family !== 'IPv4' && family !== 4) 89 | address = `[${address}]`; 90 | if (!ip || ip.length > address.length) 91 | ip = address; 92 | } 93 | } 94 | } 95 | return ip; 96 | } 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polytype", 3 | "version": "0.17.0", 4 | "description": "Dynamic multiple inheritance for JavaScript and TypeScript. Without mixins.", 5 | "keywords": [ 6 | "class", 7 | "classes", 8 | "es2020", 9 | "inheritance", 10 | "multiple-inheritance" 11 | ], 12 | "homepage": "https://github.com/fasttime/Polytype", 13 | "bugs": "https://github.com/fasttime/Polytype/issues", 14 | "license": "ISC", 15 | "author": "Francesco Trotta (https://github.com/fasttime)", 16 | "files": [ 17 | "global", 18 | "lib", 19 | "!.*" 20 | ], 21 | "main": "lib/polytype.cjs", 22 | "module": "lib/polytype.mjs", 23 | "directories": { 24 | "lib": "lib", 25 | "example": "example", 26 | "test": "test" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/fasttime/Polytype.git" 31 | }, 32 | "scripts": { 33 | "build": "npm install && gulp", 34 | "lint": "gulp lint", 35 | "release": "npm version -f -m \"Version %s\" --tag-version-prefix=", 36 | "test": "test/node-spec-runner.mjs", 37 | "test-serve": "test/serve.mjs", 38 | "version": "gulp make-toc && git add changelog.md readme.md" 39 | }, 40 | "devDependencies": { 41 | "@origin-1/eslint-config": "latest", 42 | "@rollup/plugin-terser": "latest", 43 | "ansi-colors": "latest", 44 | "c8js": "latest", 45 | "chai": "4", 46 | "eslint-formatter-compact": "latest", 47 | "eslint-plugin-eslint-env": "latest", 48 | "glob": "latest", 49 | "globals": "latest", 50 | "gulp": "latest", 51 | "gulp-eslint-new": "latest", 52 | "handlebars": "latest", 53 | "markdown-toc": "latest", 54 | "mocha": "latest", 55 | "mocha-bar": "latest", 56 | "postrequire": "latest", 57 | "rollup": "latest", 58 | "rollup-plugin-cleanup": "latest", 59 | "sync-readable": "latest", 60 | "typescript": "latest", 61 | "typescript_4.7": "npm:typescript@4.7", 62 | "typescript_4.8": "npm:typescript@4.8", 63 | "typescript_4.9": "npm:typescript@4.9", 64 | "typescript_5.0": "npm:typescript@5.0", 65 | "typescript_5.1": "npm:typescript@5.1", 66 | "typescript_5.2": "npm:typescript@5.2", 67 | "typescript_5.3": "npm:typescript@5.3", 68 | "typescript_5.4": "npm:typescript@5.4", 69 | "typescript_5.5": "npm:typescript@5.5", 70 | "typescript_5.6": "npm:typescript@5.6", 71 | "typescript_5.7": "npm:typescript@5.7", 72 | "typescript_5.8": "npm:typescript@5.8", 73 | "typescript_5.9": "npm:typescript@5.9" 74 | }, 75 | "engines": { 76 | "node": ">=16.0.0" 77 | }, 78 | "exports": { 79 | ".": { 80 | "types": "./lib/polytype-module.d.ts", 81 | "import": "./lib/polytype.mjs", 82 | "require": "./lib/polytype.cjs" 83 | }, 84 | "./global": { 85 | "types": "./lib/polytype-global.d.ts", 86 | "default": "./lib/polytype.js" 87 | }, 88 | "./lib/polytype.cjs": { 89 | "types": "./lib/polytype-module.d.ts", 90 | "default": "./lib/polytype.cjs" 91 | }, 92 | "./lib/polytype.js": { 93 | "types": "./lib/polytype-global.d.ts", 94 | "default": "./lib/polytype.js" 95 | }, 96 | "./lib/polytype.min.js": { 97 | "types": "./lib/polytype-global.d.ts", 98 | "default": "./lib/polytype.min.js" 99 | }, 100 | "./lib/polytype.min.mjs": { 101 | "types": "./lib/polytype-module.d.ts", 102 | "default": "./lib/polytype.min.mjs" 103 | }, 104 | "./lib/polytype.mjs": { 105 | "types": "./lib/polytype-module.d.ts", 106 | "default": "./lib/polytype.mjs" 107 | }, 108 | "./package.json": "./package.json" 109 | }, 110 | "imports": { 111 | "#eslint-plugin-tstest": "./test/eslint-plugin-tstest.mjs" 112 | }, 113 | "types": "lib/polytype-module.d.ts" 114 | } 115 | -------------------------------------------------------------------------------- /test/spec/common/instanceof.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* 3 | global 4 | assert 5 | classes 6 | createFunctionFromConstructor 7 | createNullPrototypeFunction 8 | maybeIt 9 | newRealm 10 | */ 11 | 12 | 'use strict'; 13 | 14 | describe 15 | ( 16 | 'instanceof', 17 | () => 18 | { 19 | beforeEach 20 | ( 21 | () => 22 | { 23 | delete Function[Symbol.hasInstance]; 24 | delete Object[Symbol.hasInstance]; 25 | }, 26 | ); 27 | 28 | describe 29 | ( 30 | 'works at instance level', 31 | () => 32 | { 33 | it 34 | ( 35 | 'with all supertypes', 36 | () => 37 | { 38 | const A = createNullPrototypeFunction('A'); 39 | const B = class { }; 40 | const C = { __proto__: B, prototype: { __proto__: B.prototype } }; 41 | const D = Function(); 42 | Object.setPrototypeOf(D, C); 43 | D.prototype = { __proto__: C.prototype }; 44 | const _AD = classes(A, D); 45 | const E = class extends _AD { }; 46 | const e = new E(); 47 | assert.instanceOf(e, B); 48 | assert.instanceOf(e, D); 49 | assert.instanceOf(e, _AD); 50 | assert.instanceOf(e, E); 51 | assert.instanceOf(e, Object); 52 | }, 53 | ); 54 | 55 | maybeIt 56 | ( 57 | newRealm, 58 | 'across realms', 59 | async () => 60 | { 61 | const { Function: Functionʼ, Object: Objectʼ } = await newRealm(); 62 | const Foo = createFunctionFromConstructor(Functionʼ); 63 | const Bar = class { }; 64 | const FooBar = class extends classes(Foo, Bar) { }; 65 | const foobar = new FooBar(); 66 | assert.instanceOf(foobar, Object); 67 | assert.instanceOf(foobar, Objectʼ); 68 | }, 69 | ); 70 | 71 | it 72 | ( 73 | 'with bound types', 74 | () => 75 | { 76 | const A = Function(); 77 | const Aˀ = A.bind(); 78 | A.prototype = { __proto__: null }; 79 | const B = class extends A { }; 80 | const Bˀ = B.bind(); 81 | classes(B); 82 | const a = new A(); 83 | assert.instanceOf(a, Aˀ); 84 | assert.notInstanceOf(a, Bˀ); 85 | }, 86 | ); 87 | }, 88 | ); 89 | 90 | describe 91 | ( 92 | 'works at class level', 93 | () => 94 | { 95 | it 96 | ( 97 | 'in the same realm', 98 | () => 99 | { 100 | const A = Function(); 101 | const B = class extends classes(A) { }; 102 | assert.instanceOf(A, Function); 103 | assert.instanceOf(B, Function); 104 | }, 105 | ); 106 | 107 | maybeIt 108 | ( 109 | newRealm, 110 | 'across realms', 111 | async () => 112 | { 113 | const { Function: Functionʼ } = await newRealm(); 114 | const A = Function(); 115 | const Aʼ = createFunctionFromConstructor(Functionʼ); 116 | const B = class extends classes(A, Aʼ) { }; 117 | assert.notInstanceOf(A, Functionʼ); 118 | assert.notInstanceOf(Aʼ, Function); 119 | assert.instanceOf(Aʼ, Functionʼ); 120 | assert.instanceOf(B, Function); 121 | assert.instanceOf(B, Functionʼ); 122 | }, 123 | ); 124 | }, 125 | ); 126 | }, 127 | ); 128 | -------------------------------------------------------------------------------- /example/ColoredCircle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Polytype JavaScript example code for Node.js. 3 | * 4 | * Requires Node.js >= 16. 5 | * 6 | * Run from command line with: 7 | * node example/ColoredCircle.js 8 | */ 9 | 10 | "use strict"; 11 | 12 | const { classes, getPrototypeListOf } = require("polytype"); 13 | 14 | class Circle 15 | { 16 | constructor(centerX, centerY, radius = 1) 17 | { 18 | this.moveTo(centerX, centerY); 19 | this.radius = radius; 20 | } 21 | get diameter() { return this.radius * 2; } 22 | set diameter(diameter) { this.radius = diameter / 2; } 23 | moveTo(centerX, centerY) 24 | { 25 | this.centerX = centerX; 26 | this.centerY = centerY; 27 | } 28 | reset() 29 | { 30 | this.moveTo(0, 0); 31 | this.radius = 1; 32 | } 33 | toString() 34 | { 35 | return `circle with center (${this.centerX}, ${this.centerY}) and radius ${this.radius}`; 36 | } 37 | } 38 | 39 | class ColoredObject 40 | { 41 | constructor(color) { this.color = color; } 42 | static areSameColor(obj1, obj2) { return obj1.color === obj2.color; } 43 | paint() { console.log(`painting in ${this.color}`); } 44 | reset() { this.color = "white"; } 45 | toString() { return `${this.color} object`; } 46 | } 47 | 48 | class ColoredCircle 49 | extends classes(Circle, ColoredObject) // Base classes as comma‐separated params 50 | { 51 | constructor(centerX, centerY, radius, color) 52 | { 53 | super 54 | ( 55 | [centerX, centerY, radius], // Circle constructor params 56 | [color] // ColoredObject constructor params 57 | ); 58 | } 59 | paint() 60 | { 61 | super.paint(); // Using method paint from some base class 62 | } 63 | reset() 64 | { 65 | for (const baseClass of getPrototypeListOf(ColoredCircle)) 66 | baseClass.reset(); 67 | } 68 | toString() 69 | { 70 | // Using method toString from base class Circle 71 | const circleString = super.class(Circle).toString(); 72 | return `${circleString} in ${this.color}`; 73 | } 74 | } 75 | 76 | class GreenCircle 77 | extends classes(Circle, ColoredObject) 78 | { 79 | constructor(centerX, centerY, radius) 80 | { 81 | super 82 | ( 83 | { super: ColoredObject, arguments: ["green"] }, 84 | { super: Circle, arguments: [centerX, centerY, radius] } 85 | ); 86 | } 87 | } 88 | 89 | class WhiteUnitCircle 90 | extends classes(Circle, ColoredObject) 91 | { 92 | constructor() 93 | { 94 | super(); // Base constructors invoked without parameters 95 | this.centerX = 0; 96 | this.centerY = 0; 97 | // The radius has been already set to 1 by the Circle constructor. 98 | this.color = "white"; 99 | } 100 | } 101 | 102 | const c = new ColoredCircle(); 103 | 104 | c.moveTo(42, 31); 105 | c.radius = 2; 106 | c.color = "red"; 107 | console.log(c.centerX, c.centerY); // 42, 31 108 | console.log(c.diameter); // 4 109 | c.paint(); // "painting in red" 110 | 111 | console.log(c instanceof Circle); // true 112 | console.log(c instanceof ColoredObject); // true 113 | console.log(c instanceof ColoredCircle); // true 114 | console.log(c instanceof Object); // true 115 | console.log(c instanceof Array); // false 116 | 117 | console.log(ColoredCircle.prototype instanceof Circle); // true 118 | console.log(ColoredCircle.prototype instanceof ColoredObject); // true 119 | console.log(ColoredCircle.prototype instanceof ColoredCircle); // false 120 | console.log(ColoredCircle.prototype instanceof Object); // true 121 | console.log(Circle.prototype instanceof ColoredObject); // false 122 | 123 | console.log("moveTo" in c); // true 124 | console.log("paint" in c); // true 125 | 126 | console.log("areSameColor" in ColoredCircle); // true 127 | console.log("areSameColor" in Circle); // false 128 | console.log("areSameColor" in ColoredObject); // true 129 | 130 | console.log(Circle.prototype.isPrototypeOf(c)); // true 131 | console.log(ColoredObject.prototype.isPrototypeOf(c)); // true 132 | console.log(ColoredCircle.prototype.isPrototypeOf(c)); // true 133 | console.log(Object.prototype.isPrototypeOf(c)); // true 134 | console.log(Array.prototype.isPrototypeOf(c)); // false 135 | 136 | console.log(Circle.isPrototypeOf(ColoredCircle)); // true 137 | console.log(ColoredObject.isPrototypeOf(ColoredCircle)); // true 138 | console.log(ColoredCircle.isPrototypeOf(ColoredCircle)); // false 139 | console.log(Object.isPrototypeOf(ColoredCircle)); // false 140 | console.log(Function.prototype.isPrototypeOf(ColoredCircle)); // true 141 | 142 | function getBaseNames(derivedClass) 143 | { 144 | return getPrototypeListOf(derivedClass).map(({ name }) => name); 145 | } 146 | 147 | console.log(getBaseNames(ColoredCircle)); // ["Circle", "ColoredObject"] 148 | console.log(getBaseNames(Int8Array)); // ["TypedArray"] 149 | console.log(getBaseNames(Circle)); // [""] i.e. [Function.prototype.name] 150 | 151 | Circle.prototype.sayHello = () => console.log("Hello!"); 152 | c.sayHello(); // "Hello!" 153 | -------------------------------------------------------------------------------- /example/ColoredCircle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Polytype TypeScript example code for Node.js. 3 | * 4 | * Requires Node.js >= 16, TypeScript >= 4.7. 5 | * 6 | * Run from command line with: 7 | * npx ts-node example/ColoredCircle.js 8 | */ 9 | 10 | import { classes, getPrototypeListOf } from "polytype"; 11 | 12 | class Circle 13 | { 14 | public centerX: number | undefined; 15 | public centerY: number | undefined; 16 | public constructor(centerX?: number, centerY?: number, public radius = 1) 17 | { 18 | this.moveTo(centerX, centerY); 19 | } 20 | public get diameter(): number { return this.radius * 2; } 21 | public set diameter(diameter: number) { this.radius = diameter / 2; } 22 | public moveTo(centerX?: number, centerY?: number): void 23 | { 24 | this.centerX = centerX; 25 | this.centerY = centerY; 26 | } 27 | public reset(): void 28 | { 29 | this.moveTo(0, 0); 30 | this.radius = 1; 31 | } 32 | public toString(): string 33 | { 34 | return `circle with center (${this.centerX}, ${this.centerY}) and radius ${this.radius}`; 35 | } 36 | } 37 | 38 | class ColoredObject 39 | { 40 | public constructor(public color?: string) { } 41 | public static areSameColor(obj1: ColoredObject, obj2: ColoredObject): boolean 42 | { 43 | return obj1.color === obj2.color; 44 | } 45 | public paint(): void { console.log(`painting in ${this.color}`); } 46 | public reset(): void { this.color = "white"; } 47 | public toString(): string { return `${this.color} object`; } 48 | } 49 | 50 | class ColoredCircle 51 | extends classes(Circle, ColoredObject) // Base classes as comma‐separated params 52 | { 53 | public constructor(centerX?: number, centerY?: number, radius?: number, color?: string) 54 | { 55 | super 56 | ( 57 | [centerX, centerY, radius], // Circle constructor params 58 | [color], // ColoredObject constructor params 59 | ); 60 | } 61 | public paint(): void 62 | { 63 | super.paint(); // Using method paint from some base class 64 | } 65 | public reset(): void 66 | { 67 | for (const baseClass of getPrototypeListOf(ColoredCircle) as { reset(): void; }[]) 68 | baseClass.reset(); 69 | } 70 | public toString(): string 71 | { 72 | // Using method toString from base class Circle 73 | const circleString = super.class(Circle).toString(); 74 | return `${circleString} in ${this.color}`; 75 | } 76 | } 77 | 78 | class GreenCircle 79 | extends classes(Circle, ColoredObject) 80 | { 81 | public constructor(centerX: number, centerY: number, radius: number) 82 | { 83 | super 84 | ( 85 | { super: ColoredObject, arguments: ["green"] }, 86 | { super: Circle, arguments: [centerX, centerY, radius] }, 87 | ); 88 | } 89 | } 90 | 91 | class WhiteUnitCircle 92 | extends classes(Circle, ColoredObject) 93 | { 94 | public constructor() 95 | { 96 | super(); // Base constructors invoked without parameters 97 | this.centerX = 0; 98 | this.centerY = 0; 99 | // The radius has been already set to 1 by the Circle constructor. 100 | this.color = "white"; 101 | } 102 | } 103 | 104 | const c = new ColoredCircle(); 105 | 106 | c.moveTo(42, 31); 107 | c.radius = 2; 108 | c.color = "red"; 109 | console.log(c.centerX, c.centerY); // 42, 31 110 | console.log(c.diameter); // 4 111 | c.paint(); // "painting in red" 112 | 113 | console.log(c instanceof Circle); // true 114 | console.log(c instanceof ColoredObject); // true 115 | console.log(c instanceof ColoredCircle); // true 116 | console.log(c instanceof Object); // true 117 | console.log(c instanceof Array); // false 118 | 119 | console.log(ColoredCircle.prototype instanceof Circle); // true 120 | console.log(ColoredCircle.prototype instanceof ColoredObject); // true 121 | console.log(ColoredCircle.prototype instanceof ColoredCircle); // false 122 | console.log(ColoredCircle.prototype instanceof Object); // true 123 | console.log(Circle.prototype instanceof ColoredObject); // false 124 | 125 | console.log("moveTo" in c); // true 126 | console.log("paint" in c); // true 127 | 128 | console.log("areSameColor" in ColoredCircle); // true 129 | console.log("areSameColor" in Circle); // false 130 | console.log("areSameColor" in ColoredObject); // true 131 | 132 | console.log(Circle.prototype.isPrototypeOf(c)); // true 133 | console.log(ColoredObject.prototype.isPrototypeOf(c)); // true 134 | console.log(ColoredCircle.prototype.isPrototypeOf(c)); // true 135 | console.log(Object.prototype.isPrototypeOf(c)); // true 136 | console.log(Array.prototype.isPrototypeOf(c)); // false 137 | 138 | console.log(Circle.isPrototypeOf(ColoredCircle)); // true 139 | console.log(ColoredObject.isPrototypeOf(ColoredCircle)); // true 140 | console.log(ColoredCircle.isPrototypeOf(ColoredCircle)); // false 141 | console.log(Object.isPrototypeOf(ColoredCircle)); // false 142 | console.log(Function.prototype.isPrototypeOf(ColoredCircle)); // true 143 | 144 | function getBaseNames(derivedClass: new () => unknown): string[] 145 | { 146 | return getPrototypeListOf(derivedClass).map(({ name }: { name: string; }): string => name); 147 | } 148 | 149 | console.log(getBaseNames(ColoredCircle)); // ["Circle", "ColoredObject"] 150 | console.log(getBaseNames(Int8Array)); // ["TypedArray"] 151 | console.log(getBaseNames(Circle)); // [""] i.e. [Function.prototype.name] 152 | 153 | type Hello = T & { sayHello(): void; }; 154 | (Circle.prototype as Hello).sayHello = (): void => console.log("Hello!"); 155 | (c as Hello).sayHello(); // "Hello!" 156 | -------------------------------------------------------------------------------- /test/spec/common/function-prototype-bind.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert classes createDeceptiveObject maybeIt newRealm */ 3 | 4 | 'use strict'; 5 | 6 | describe 7 | ( 8 | 'Function.prototype.bind', 9 | () => 10 | { 11 | before(() => classes(Object)); 12 | 13 | maybeIt 14 | ( 15 | newRealm, 16 | 'preserves the original enumerable and writable attributes', 17 | async () => 18 | { 19 | const { Function: Functionʼ } = await newRealm(); 20 | const { Function: Functionʼʼ } = await newRealm(); 21 | Object.defineProperty 22 | ( 23 | Functionʼ.prototype, 24 | 'bind', 25 | { 26 | configurable: true, 27 | enumerable: true, 28 | value: Functionʼ.prototype.bind, 29 | writable: false, 30 | }, 31 | ); 32 | Object.defineProperty 33 | ( 34 | Functionʼʼ.prototype, 35 | 'bind', 36 | { 37 | configurable: true, 38 | enumerable: false, 39 | value: Functionʼʼ.prototype.bind, 40 | writable: true, 41 | }, 42 | ); 43 | const Aʼ = Functionʼ(); 44 | const Aʼʼ = Functionʼʼ(); 45 | classes(Aʼ, Aʼʼ); 46 | assert.hasOwnPropertyDescriptor 47 | ( 48 | Functionʼ.prototype, 49 | 'bind', 50 | { 51 | configurable: true, 52 | enumerable: true, 53 | value: Functionʼ.prototype.bind, 54 | writable: false, 55 | }, 56 | ); 57 | assert.hasOwnPropertyDescriptor 58 | ( 59 | Functionʼʼ.prototype, 60 | 'bind', 61 | { 62 | configurable: true, 63 | enumerable: false, 64 | value: Functionʼʼ.prototype.bind, 65 | writable: true, 66 | }, 67 | ); 68 | }, 69 | ); 70 | 71 | it 72 | ( 73 | 'is not defined if a constructor prototype cannot be determined', 74 | () => 75 | { 76 | const emptyObj = { __proto__: null }; 77 | const Foo = Function(); 78 | Foo.constructor = { __proto__: emptyObj }; 79 | classes(Foo); 80 | assert.notOwnProperty(emptyObj, 'bind'); 81 | }, 82 | ); 83 | 84 | it 85 | ( 86 | 'has expected own properties', 87 | () => 88 | assert.hasOwnPropertyDescriptors 89 | ( 90 | Function.prototype.bind, 91 | { 92 | length: 93 | { 94 | configurable: true, 95 | enumerable: false, 96 | value: 1, 97 | writable: false, 98 | }, 99 | name: 100 | { 101 | configurable: true, 102 | enumerable: false, 103 | value: 'bind', 104 | writable: false, 105 | }, 106 | }, 107 | ), 108 | ); 109 | 110 | it 111 | ( 112 | 'has expected prototype', 113 | () => 114 | assert.strictEqual(Object.getPrototypeOf(Function.prototype.bind), Function.prototype), 115 | ); 116 | 117 | it 118 | ( 119 | 'looks like a native function', 120 | () => 121 | { 122 | const str = Function.prototype.toString.call(Function.prototype.bind); 123 | assert.match(str, /^function (?!bind\b)\w*\(\) {\s+\[native code]\s+}$/); 124 | }, 125 | ); 126 | 127 | it 128 | ( 129 | 'cannot be called with new', 130 | () => 131 | // eslint-disable-next-line new-cap 132 | assert.throwsTypeError(() => new Function.prototype.bind(), /\bis not a constructor\b/), 133 | ); 134 | 135 | it 136 | ( 137 | 'throws a TypeError if this is not a function', 138 | () => 139 | assert.throwsTypeError 140 | ( 141 | () => Function.prototype.bind.call({ }), 142 | [ 143 | // V8 144 | 'Bind must be called on a function', 145 | 146 | // Firefox 112 or later 147 | 'Function.prototype.bind called on incompatible Object', 148 | 149 | // Firefox 111 or earlier 150 | 'Function.prototype.bind called on incompatible target', 151 | 152 | // Safari 153 | '|this| is not a function inside Function.prototype.bind', 154 | ], 155 | ), 156 | ); 157 | 158 | it 159 | ( 160 | 'throws a TypeError with a deceptive object', 161 | () => 162 | { 163 | const obj = createDeceptiveObject(); 164 | assert.throwsTypeError 165 | (() => Object.bind(obj), 'Corrupt inquiry result'); 166 | }, 167 | ); 168 | }, 169 | ); 170 | -------------------------------------------------------------------------------- /test/spec/common/object-prototype-is-prototype-of.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert classes createDeceptiveObject maybeIt newRealm */ 3 | 4 | 'use strict'; 5 | 6 | describe 7 | ( 8 | 'Object.prototype.isPrototypeOf', 9 | () => 10 | { 11 | before(() => classes(Object)); 12 | 13 | maybeIt 14 | ( 15 | newRealm, 16 | 'preserves the original enumerable and writable attributes', 17 | async () => 18 | { 19 | const { Function: Functionʼ, Object: Objectʼ } = await newRealm(); 20 | const { Function: Functionʼʼ, Object: Objectʼʼ } = await newRealm(); 21 | Object.defineProperty 22 | ( 23 | Objectʼ.prototype, 24 | 'isPrototypeOf', 25 | { 26 | configurable: true, 27 | enumerable: true, 28 | value: Objectʼ.prototype.isPrototypeOf, 29 | writable: false, 30 | }, 31 | ); 32 | Object.defineProperty 33 | ( 34 | Objectʼʼ.prototype, 35 | 'isPrototypeOf', 36 | { 37 | configurable: true, 38 | enumerable: false, 39 | value: Objectʼʼ.prototype.isPrototypeOf, 40 | writable: true, 41 | }, 42 | ); 43 | const Aʼ = Functionʼ(); 44 | const Aʼʼ = Functionʼʼ(); 45 | classes(Aʼ, Aʼʼ); 46 | assert.hasOwnPropertyDescriptor 47 | ( 48 | Objectʼ.prototype, 49 | 'isPrototypeOf', 50 | { 51 | configurable: true, 52 | enumerable: true, 53 | value: Objectʼ.prototype.isPrototypeOf, 54 | writable: false, 55 | }, 56 | ); 57 | assert.hasOwnPropertyDescriptor 58 | ( 59 | Objectʼʼ.prototype, 60 | 'isPrototypeOf', 61 | { 62 | configurable: true, 63 | enumerable: false, 64 | value: Objectʼʼ.prototype.isPrototypeOf, 65 | writable: true, 66 | }, 67 | ); 68 | }, 69 | ); 70 | 71 | it 72 | ( 73 | 'is not defined if a root prototype cannot be determined', 74 | () => 75 | { 76 | const emptyObj = { __proto__: null }; 77 | const Foo = Function(); 78 | Object.setPrototypeOf(Foo, emptyObj); 79 | classes(Foo); 80 | assert.notOwnProperty(emptyObj, 'isPrototypeOf'); 81 | }, 82 | ); 83 | 84 | it 85 | ( 86 | 'has expected own properties', 87 | () => 88 | assert.hasOwnPropertyDescriptors 89 | ( 90 | Object.prototype.isPrototypeOf, 91 | { 92 | length: 93 | { 94 | configurable: true, 95 | enumerable: false, 96 | value: 1, 97 | writable: false, 98 | }, 99 | name: 100 | { 101 | configurable: true, 102 | enumerable: false, 103 | value: 'isPrototypeOf', 104 | writable: false, 105 | }, 106 | }, 107 | ), 108 | ); 109 | 110 | it 111 | ( 112 | 'has expected prototype', 113 | () => 114 | assert.strictEqual 115 | (Object.getPrototypeOf(Object.prototype.isPrototypeOf), Function.prototype), 116 | ); 117 | 118 | it 119 | ( 120 | 'looks like a native function', 121 | () => 122 | { 123 | const str = Function.prototype.toString.call(Object.prototype.isPrototypeOf); 124 | assert.match(str, /^function (?!isPrototypeOf\b)\w*\(\) {\s+\[native code]\s+}$/); 125 | }, 126 | ); 127 | 128 | it 129 | ( 130 | 'cannot be called with new', 131 | () => 132 | assert.throwsTypeError 133 | (() => new Object.prototype.isPrototypeOf(), /\bis not a constructor\b/), 134 | ); 135 | 136 | it 137 | ( 138 | 'throws a TypeError if this is null', 139 | () => 140 | { 141 | const fn = Object.prototype.isPrototypeOf.bind(null, { }); 142 | assert.throwsTypeError(fn); 143 | }, 144 | ); 145 | 146 | it 147 | ( 148 | 'throws a TypeError if this is undefined', 149 | () => 150 | { 151 | const fn = Object.prototype.isPrototypeOf.bind(undefined, { }); 152 | assert.throwsTypeError(fn); 153 | }, 154 | ); 155 | 156 | it 157 | ( 158 | 'throws a TypeError with a deceptive object', 159 | () => 160 | { 161 | { 162 | const obj = createDeceptiveObject(); 163 | assert.throwsTypeError 164 | (() => Object.isPrototypeOf(obj), 'Corrupt inquiry result'); 165 | } 166 | { 167 | const obj = createDeceptiveObject(null); 168 | assert.throwsTypeError(() => Object.isPrototypeOf(obj)); 169 | } 170 | }, 171 | ); 172 | }, 173 | ); 174 | -------------------------------------------------------------------------------- /src/polytype.d.ts.hbs: -------------------------------------------------------------------------------- 1 | // Type definitions for Polytype {{version}} 2 | 3 | declare namespace Polytype 4 | { 5 | // Helpers ///////////////////////////////////////////////////////////////////////////////////// 6 | 7 | type Enrich = MainType & Omit; 8 | 9 | type EnrichMore = 10 | EnrichMoreProps[ExtraTypes extends [] ? 0 : 1]; 11 | 12 | interface EnrichMoreProps 13 | { 14 | 0: MainType; 15 | 1: 16 | ExtraTypes extends [infer ExtraHead, ...infer ExtraTail] ? 17 | EnrichMore, ExtraTail> : never; 18 | } 19 | 20 | type IntersectionOf = 21 | UnboxedIntersectionOf<{ [key in keyof T]: [T[key]]; }> extends { 0: infer U; } ? U : never; 22 | 23 | type NonEmptyArray = [T, ...T[]]; 24 | 25 | type ProtoType = T extends { prototype: infer U; } ? U : never; 26 | 27 | type ReadonlySuperConstructorParameters = 28 | Readonly unknown) & T>>; 29 | 30 | type UnboxedIntersectionOf = 31 | UnionOf extends infer U ? 32 | (U extends unknown ? (arg: U) => unknown : never) extends (arg: infer V) => unknown ? 33 | V : never : 34 | never; 35 | 36 | type UnionOf = T[number]; 37 | 38 | // Implementation related ////////////////////////////////////////////////////////////////////// 39 | 40 | type AsSuperConstructor = Extract; 41 | 42 | type ClusteredConstructor = 43 | { 44 | readonly prototype: ProtoType>; 45 | 46 | new (...args: MapTupleTypesToOptionalReadonlyConstructorParameters): 47 | ClusteredPrototype; 48 | 49 | new (...args: UnionOf>[]): 50 | ClusteredPrototype; 51 | } 52 | & 53 | EnrichMore>, T>; 54 | 55 | type ClusteredPrototype = 56 | SuperPrototypeSelector> & 57 | IntersectionOf<{ [key in keyof T]: InstanceType; }>; 58 | 59 | type MapTupleTypesToOptionalReadonlyConstructorParameters = 60 | { [key in keyof T]?: ReadonlySuperConstructorParameters>; }; 61 | 62 | type MapTupleTypesToReadonlySuperConstructorInvokeInfo = 63 | { [key in keyof T]: Readonly>>; }; 64 | 65 | type SuperConstructor = abstract new (...args: any) => object; 66 | 67 | class SuperConstructorSelector 68 | { 69 | /** 70 | * Allows accessing a static property or calling a static method in a specified base class, 71 | * eliminating ambiguity when multiple base classes share a property with the same key. 72 | * 73 | * @param type 74 | * 75 | * The referenced base class. 76 | */ 77 | protected class(type: U): U; 78 | } 79 | 80 | class SuperPrototypeSelector 81 | { 82 | /** 83 | * Allows accessing an instance property or calling an instance method in a specified base 84 | * class, eliminating ambiguity when multiple base classes share a property with the same 85 | * key. 86 | * 87 | * @param type 88 | * 89 | * The referenced base class. 90 | */ 91 | protected class(type: U): InstanceType; 92 | } 93 | } 94 | 95 | /** Specifies the arguments used to call a base class constructor. */ 96 | export interface SuperConstructorInvokeInfo 97 | { 98 | /** The base class being referenced. */ 99 | super: T; 100 | 101 | /** 102 | * An array specifying the arguments with which the base class constructor should be called. 103 | * If undefined, the base class constructor will be called without any arguments. 104 | */ 105 | arguments?: Polytype.ReadonlySuperConstructorParameters | undefined; 106 | } 107 | 108 | {{#* inline "classes"}} 109 | /** Allows defining a derived class that inherits from multiple base classes. */ 110 | {{q}}classes>(...types: T): 111 | Polytype.ClusteredConstructor; 112 | {{/inline}} 113 | {{#* inline "getPrototypeListOf"}} 114 | /** 115 | * Returns a list of prototypes of an object. 116 | * * For objects with a regular non‐null prototype, an array containing the prototype as its only 117 | * element is returned. 118 | * * For objects with a null prototype, an empty array is returned. 119 | * * For constructors and instance prototypes based on Polytype clustered objects, an array 120 | * containing all zero or more prototypes of the object is returned. 121 | * 122 | * @param o 123 | * 124 | * The object that references the prototypes. 125 | */ 126 | {{q}}getPrototypeListOf(o: any): any[]; 127 | {{/inline}} 128 | {{#if asModule}} 129 | {{> classes q="export function "}} 130 | 131 | /** 132 | * Globally defines or undefines `classes` and `Object.getPrototypeListOf`. 133 | * 134 | * Calling this function without arguments or with a falsy argument allows using Polytype everywhere 135 | * inside the current JavaScript realm without imports. 136 | * Calling it again with a truthy argument reverts the changes made by the previous call. 137 | * 138 | * This function is only available in the module versions of Polytype. 139 | * For most purposes it is better to import the global version of Polytype directly rather than 140 | * calling this function. 141 | * 142 | * @param undo 143 | * 144 | * If this argument is falsy or unspecified, `classes` and `Object.getPrototypeListOf` will be 145 | * defined globally. 146 | * If this argument is truthy, the globals `classes` and `Object.getPrototypeListOf` will be 147 | * deleted. 148 | * 149 | * @returns 150 | * 151 | * `true` if any changes were made; `false` otherwise. 152 | */ 153 | export function defineGlobally(uninstall?: boolean): boolean; 154 | 155 | {{> getPrototypeListOf q="export function "}} 156 | {{else}} 157 | declare global 158 | { 159 | {{> classes q="function "}} 160 | 161 | interface ObjectConstructor 162 | { 163 | {{> getPrototypeListOf}} 164 | } 165 | } 166 | {{/if}} 167 | -------------------------------------------------------------------------------- /test/spec/common/is-prototype-of-call.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert classes document maybeIt newRealm */ 3 | 4 | 'use strict'; 5 | 6 | describe 7 | ( 8 | '?.isPrototypeOf(?)', 9 | () => 10 | { 11 | before(() => classes(Object)); 12 | 13 | it 14 | ( 15 | 'returns false with null argument', 16 | () => 17 | { 18 | const actual = Object.prototype.isPrototypeOf.call(undefined, null); 19 | assert.isFalse(actual); 20 | }, 21 | ); 22 | 23 | it 24 | ( 25 | 'returns false with undefined argument', 26 | () => 27 | { 28 | const actual = Object.prototype.isPrototypeOf.call(null, undefined); 29 | assert.isFalse(actual); 30 | }, 31 | ); 32 | 33 | it 34 | ( 35 | 'returns false with boolean type argument', 36 | () => 37 | { 38 | const actual = Boolean.prototype.isPrototypeOf(true); 39 | assert.isFalse(actual); 40 | }, 41 | ); 42 | 43 | it 44 | ( 45 | 'returns false with number type argument', 46 | () => 47 | { 48 | const actual = Number.prototype.isPrototypeOf(1); 49 | assert.isFalse(actual); 50 | }, 51 | ); 52 | 53 | it 54 | ( 55 | 'returns false with bigint type argument', 56 | () => 57 | { 58 | const actual = BigInt.prototype.isPrototypeOf(BigInt(1)); 59 | assert.isFalse(actual); 60 | }, 61 | ); 62 | 63 | it 64 | ( 65 | 'returns false with string type argument', 66 | () => 67 | { 68 | const actual = String.prototype.isPrototypeOf('foo'); 69 | assert.isFalse(actual); 70 | }, 71 | ); 72 | 73 | it 74 | ( 75 | 'returns false with Symbol type argument', 76 | () => 77 | { 78 | const actual = Symbol.prototype.isPrototypeOf(Symbol.iterator); 79 | assert.isFalse(actual); 80 | }, 81 | ); 82 | 83 | it 84 | ( 85 | 'returns false when this and the argument are the same object', 86 | () => 87 | { 88 | const obj = { }; 89 | const actual = obj.isPrototypeOf(obj); 90 | assert.isFalse(actual); 91 | }, 92 | ); 93 | 94 | it 95 | ( 96 | 'returns true when this is the argument prototype', 97 | () => 98 | { 99 | const actual = Function.prototype.isPrototypeOf(Object); 100 | assert.isTrue(actual); 101 | }, 102 | ); 103 | 104 | it 105 | ( 106 | 'returns true when this is in the argument prototype chain', 107 | () => 108 | { 109 | const actual = Object.prototype.isPrototypeOf(Object); 110 | assert.isTrue(actual); 111 | }, 112 | ); 113 | 114 | maybeIt 115 | ( 116 | typeof document !== 'undefined', 117 | 'returns true with document.all', 118 | () => 119 | { 120 | const actual = Object.prototype.isPrototypeOf(document.all); 121 | assert.isTrue(actual); 122 | }, 123 | ); 124 | 125 | describe 126 | ( 127 | 'works with subtypes', 128 | () => 129 | { 130 | function test(classes) 131 | { 132 | class A 133 | { } 134 | 135 | class B 136 | { } 137 | 138 | class C extends classes(A, B) 139 | { } 140 | 141 | class D 142 | { } 143 | 144 | class E extends classes(C, D) 145 | { } 146 | 147 | assert.isTrue(A.isPrototypeOf(C)); 148 | assert.isTrue(B.isPrototypeOf(C)); 149 | assert.isFalse(C.isPrototypeOf(C)); 150 | assert.isFalse(D.isPrototypeOf(C)); 151 | assert.isFalse(E.isPrototypeOf(C)); 152 | assert(Function.prototype.isPrototypeOf(C)); 153 | assert(Object.prototype.isPrototypeOf(C)); 154 | } 155 | 156 | it('in the same realm', () => test(classes)); 157 | 158 | maybeIt 159 | ( 160 | newRealm, 161 | 'in another realm', 162 | async () => 163 | { 164 | const { classes: classesʼ } = await newRealm(true); 165 | test(classesʼ); 166 | }, 167 | ); 168 | }, 169 | ); 170 | 171 | describe 172 | ( 173 | 'works with subprototypes', 174 | () => 175 | { 176 | function test(classes) 177 | { 178 | class A 179 | { } 180 | 181 | class B 182 | { } 183 | 184 | class C extends classes(A, B) 185 | { } 186 | 187 | class D 188 | { } 189 | 190 | class E extends classes(C, D) 191 | { } 192 | 193 | const c = new C(); 194 | assert.isTrue(A.prototype.isPrototypeOf(c)); 195 | assert.isTrue(B.prototype.isPrototypeOf(c)); 196 | assert.isTrue(C.prototype.isPrototypeOf(c)); 197 | assert.isFalse(D.prototype.isPrototypeOf(c)); 198 | assert.isFalse(E.prototype.isPrototypeOf(c)); 199 | assert.isTrue(Object.prototype.isPrototypeOf(c)); 200 | } 201 | 202 | it('in the same realm', () => test(classes)); 203 | 204 | maybeIt 205 | ( 206 | newRealm, 207 | 'in another realm', 208 | async () => 209 | { 210 | const { classes: classesʼ } = await newRealm(true); 211 | test(classesʼ); 212 | }, 213 | ); 214 | }, 215 | ); 216 | }, 217 | ); 218 | -------------------------------------------------------------------------------- /test/spec/common/get-prototype-list-of.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* 3 | global 4 | assert 5 | classes 6 | createDeceptiveObject 7 | createNullPrototypeFunction 8 | document 9 | maybeIt 10 | newRealm 11 | */ 12 | 13 | 'use strict'; 14 | 15 | describe 16 | ( 17 | 'getPrototypeListOf', 18 | () => 19 | { 20 | function testGetPrototypeListOf(obj, expected) 21 | { 22 | const actual1 = Object.getPrototypeListOf(obj); 23 | assert.deepEqual(actual1, expected); 24 | const actual2 = Object.getPrototypeListOf(obj); 25 | assert.notStrictEqual 26 | ( 27 | actual2, 28 | actual1, 29 | 'Multiple invocations of Object.getPrototypeListOf should not return the same ' + 30 | 'object.', 31 | ); 32 | } 33 | 34 | it 35 | ( 36 | 'has expected own properties', 37 | () => 38 | assert.hasOwnPropertyDescriptors 39 | ( 40 | Object.getPrototypeListOf, 41 | { 42 | length: 43 | { 44 | configurable: true, 45 | enumerable: false, 46 | value: 1, 47 | writable: false, 48 | }, 49 | name: 50 | { 51 | configurable: true, 52 | enumerable: false, 53 | value: 'getPrototypeListOf', 54 | writable: false, 55 | }, 56 | }, 57 | ), 58 | ); 59 | 60 | it 61 | ( 62 | 'has expected prototype', 63 | () => 64 | assert.strictEqual 65 | (Object.getPrototypeOf(Object.getPrototypeListOf), Function.prototype), 66 | ); 67 | 68 | it 69 | ( 70 | 'cannot be called with new', 71 | () => 72 | assert.throwsTypeError 73 | // eslint-disable-next-line new-cap 74 | (() => new Object.getPrototypeListOf(), /\bis not a constructor\b/), 75 | ); 76 | 77 | it 78 | ( 79 | 'returns a new empty array if an object has null prototype', 80 | () => testGetPrototypeListOf({ __proto__: null }, []), 81 | ); 82 | 83 | it 84 | ( 85 | 'returns a one element array if an object has a non-null prototype', 86 | () => testGetPrototypeListOf({ }, [Object.prototype]), 87 | ); 88 | 89 | it 90 | ( 91 | 'returns the prototype of a multiple inheritance instance', 92 | () => 93 | { 94 | class A 95 | { } 96 | 97 | class B 98 | { } 99 | 100 | class C extends classes(A, B) 101 | { } 102 | 103 | testGetPrototypeListOf(new C(), [C.prototype]); 104 | }, 105 | ); 106 | 107 | it 108 | ( 109 | 'returns a new empty array for a clustered constructor', 110 | () => 111 | { 112 | const A = 113 | class 114 | { }; 115 | const B = 116 | class 117 | { }; 118 | const _AB = classes(A, B); 119 | testGetPrototypeListOf(_AB, []); 120 | }, 121 | ); 122 | 123 | it 124 | ( 125 | 'returns a new empty array for a clustered prototype', 126 | () => 127 | { 128 | const A = 129 | class 130 | { }; 131 | const B = 132 | class 133 | { }; 134 | const _AB = classes(A, B); 135 | testGetPrototypeListOf(_AB.prototype, []); 136 | }, 137 | ); 138 | 139 | describe 140 | ( 141 | 'returns the prototypes of a multiple inheritance constructor', 142 | () => 143 | { 144 | function test(classes) 145 | { 146 | class A 147 | { } 148 | 149 | class B 150 | { } 151 | 152 | class C extends classes(A, B) 153 | { } 154 | 155 | testGetPrototypeListOf(C, [A, B]); 156 | } 157 | 158 | it('in the same realm', () => test(classes)); 159 | 160 | maybeIt 161 | ( 162 | newRealm, 163 | 'in another realm', 164 | async () => 165 | { 166 | const { classes: classesʼ } = await newRealm(true); 167 | test(classesʼ); 168 | }, 169 | ); 170 | }, 171 | ); 172 | 173 | describe 174 | ( 175 | 'returns all prototypes of a multiple inheritance prototype excluding null and ' + 176 | 'duplicates', 177 | () => 178 | { 179 | function test(classes) 180 | { 181 | const A = 182 | class 183 | { }; 184 | const B = createNullPrototypeFunction(); 185 | const C = 186 | class 187 | { }; 188 | const D = Function(); 189 | D.prototype = A.prototype; 190 | const ABCD = 191 | class extends classes(A, B, C, D) 192 | { }; 193 | testGetPrototypeListOf(ABCD.prototype, [A.prototype, C.prototype]); 194 | } 195 | 196 | it('in the same realm', () => test(classes)); 197 | 198 | maybeIt 199 | ( 200 | newRealm, 201 | 'in another realm', 202 | async () => 203 | { 204 | const { classes: classesʼ } = await newRealm(true); 205 | test(classesʼ); 206 | }, 207 | ); 208 | }, 209 | ); 210 | 211 | maybeIt 212 | ( 213 | typeof document !== 'undefined', 214 | 'returns a one element array if an object has document.all for prototype', 215 | () => testGetPrototypeListOf({ __proto__: document.all }, [document.all]), 216 | ); 217 | 218 | it 219 | ( 220 | 'throws a TypeError with null', 221 | () => assert.throwsTypeError(() => Object.getPrototypeListOf(null)), 222 | ); 223 | 224 | it 225 | ( 226 | 'throws a TypeError with a deceptive object', 227 | () => 228 | { 229 | { 230 | const obj = createDeceptiveObject(); 231 | assert.throwsTypeError 232 | (() => Object.getPrototypeListOf(obj), 'Corrupt inquiry result'); 233 | } 234 | { 235 | const obj = createDeceptiveObject(null); 236 | assert.throwsTypeError(() => Object.getPrototypeListOf(obj)); 237 | } 238 | }, 239 | ); 240 | }, 241 | ); 242 | -------------------------------------------------------------------------------- /test/spec/ts-defs.spec.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, node */ 2 | /* global maybeIt polytypeMode */ 3 | 4 | import assert from 'node:assert/strict'; 5 | import { readFile } from 'node:fs/promises'; 6 | import { createRequire } from 'node:module'; 7 | import { dirname, join } from 'node:path'; 8 | import { fileURLToPath } from 'node:url'; 9 | import { getImportStatement, getTestCase, processTestCase } from '#eslint-plugin-tstest'; 10 | import { glob } from 'glob'; 11 | 12 | function defineTests(typescriptPkgName) 13 | { 14 | const actualize = 15 | async () => 16 | { 17 | function doCreateProgram() 18 | { 19 | const cwd = process.cwd(); 20 | { 21 | const directory = dirname(dirname(__dirname)); 22 | process.chdir(directory); 23 | } 24 | try 25 | { 26 | const program = createProgram(fileNames, options, host); 27 | return program; 28 | } 29 | finally 30 | { 31 | process.chdir(cwd); 32 | } 33 | } 34 | 35 | const 36 | { 37 | default: 38 | { 39 | convertCompilerOptionsFromJson, 40 | createCompilerHost, 41 | createProgram, 42 | createSourceFile, 43 | flattenDiagnosticMessageText, 44 | getPreEmitDiagnostics, 45 | }, 46 | } = 47 | await import(typescriptPkgName); 48 | const { options } = convertCompilerOptionsFromJson(compilerOptions); 49 | const importStatement = getImportStatement(polytypeMode); 50 | const fileNames = []; 51 | testCases.forEach 52 | ( 53 | (testCase, index) => 54 | { 55 | testCase.actualMessages = []; 56 | fileNames.push(`:${index}`); 57 | }, 58 | ); 59 | const sourceFiles = []; 60 | const host = createCompilerHost({ }); 61 | { 62 | const { getSourceFile } = host; 63 | host.getSourceFile = 64 | (fileName, languageVersion, onError) => 65 | { 66 | let sourceFile; 67 | const match = /^:(?\d+)\.ts$/.exec(fileName); 68 | if (match) 69 | { 70 | const testCase = testCases[match.groups.baseName]; 71 | const sourceText = processTestCase(testCase, importStatement); 72 | sourceFile = createSourceFile(fileName, sourceText); 73 | sourceFile.testCase = testCase; 74 | sourceFiles.push(sourceFile); 75 | } 76 | else 77 | sourceFile = getSourceFile(fileName, languageVersion, onError); 78 | return sourceFile; 79 | }; 80 | } 81 | const program = doCreateProgram(); 82 | for (const sourceFile of sourceFiles) 83 | { 84 | const { actualMessages } = sourceFile.testCase; 85 | getPreEmitDiagnostics(program, sourceFile).forEach 86 | ( 87 | ({ messageText }) => 88 | { 89 | const message = flattenDiagnosticMessageText(messageText, '\n'); 90 | actualMessages.push(message); 91 | }, 92 | ); 93 | } 94 | }; 95 | 96 | before 97 | ( 98 | async function () 99 | { 100 | this.timeout(10000); 101 | await actualize(); 102 | }, 103 | ); 104 | 105 | testCases.forEach 106 | ( 107 | testCase => 108 | { 109 | const { expectedMessage, polytypeMode: currentPolytypeMode } = testCase; 110 | 111 | maybeIt 112 | ( 113 | currentPolytypeMode === undefined || currentPolytypeMode === polytypeMode, 114 | testCase.title, 115 | () => 116 | { 117 | const { actualMessages } = testCase; 118 | const actualErrorCount = testCase.actualMessages.length; 119 | const actualMessagesString = 120 | actualMessages.map(message => `\n${message}`).join(''); 121 | if (expectedMessage === undefined) 122 | { 123 | assert.equal 124 | ( 125 | actualErrorCount, 126 | 0, 127 | `expected no compiler errors, but got ${actualErrorCount}:` + 128 | `${actualMessagesString}`, 129 | ); 130 | } 131 | else if (Array.isArray(expectedMessage)) 132 | { 133 | assert.equal 134 | ( 135 | actualErrorCount, 136 | 1, 137 | `expected exactly 1 compiler error, but got ${actualErrorCount}:` + 138 | `${actualMessagesString}`, 139 | ); 140 | const [actualMessage] = testCase.actualMessages; 141 | assert 142 | ( 143 | expectedMessage.includes(actualMessage), 144 | `Actual message:\n${actualMessage}`, 145 | ); 146 | } 147 | else 148 | { 149 | assert.equal 150 | ( 151 | actualErrorCount, 152 | 1, 153 | `expected exactly 1 compiler error, but got ${actualErrorCount}:` + 154 | `${actualMessagesString}`, 155 | ); 156 | const [actualMessage] = testCase.actualMessages; 157 | assert.equal(actualMessage, expectedMessage); 158 | } 159 | }, 160 | ); 161 | }, 162 | ); 163 | } 164 | 165 | const __dirname = dirname(fileURLToPath(import.meta.url)); 166 | 167 | const testCases = 168 | await 169 | (async () => 170 | { 171 | async function loadTestCase(path) 172 | { 173 | const code = await readFile(path, 'utf-8'); 174 | const testCase = getTestCase(code); 175 | return testCase; 176 | } 177 | 178 | const pattern = join(__dirname, 'ts-defs', '*.tstest'); 179 | const paths = await glob(pattern); 180 | const promises = paths.map(loadTestCase); 181 | const testCases = await Promise.all(promises); 182 | return testCases; 183 | } 184 | )(); 185 | 186 | const compilerOptions = 187 | (() => 188 | { 189 | const require = createRequire(import.meta.url); 190 | const { compilerOptions } = require('../../tsconfig.json'); 191 | return compilerOptions; 192 | } 193 | )(); 194 | 195 | describe 196 | ( 197 | 'TypeScript definitions', 198 | () => 199 | { 200 | const typescriptPkgNames = 201 | (() => 202 | { 203 | const require = createRequire(import.meta.url); 204 | const { devDependencies } = require('../../package.json'); 205 | const typescriptPkgNames = 206 | Object 207 | .keys(devDependencies) 208 | .filter(devDependency => /^typescript_\d+\.\d+$/.test(devDependency)); 209 | return typescriptPkgNames; 210 | } 211 | )(); 212 | for (const typescriptPkgName of typescriptPkgNames) 213 | { 214 | const title = typescriptPkgName.replace(/^typescript_/, 'TypeScript '); 215 | describe(title, () => defineTests(typescriptPkgName)); 216 | } 217 | }, 218 | ); 219 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | 2 | ## [0.17.0](https://github.com/fasttime/Polytype/releases/tag/0.17.0) (2023-08-29) 3 | 4 | * Added support for Deno. 5 | * Dropped support for Node.js < 16. 6 | 7 | 8 | ## [0.16.2](https://github.com/fasttime/Polytype/releases/tag/0.16.2) (2023-02-21) 9 | 10 | * Fixed exports for `"polytype/lib/polytype.js"` and `"polytype/lib/polytype.min.js"` in 11 | package.json. 12 | 13 | 14 | ## [0.16.1](https://github.com/fasttime/Polytype/releases/tag/0.16.1) (2023-01-12) 15 | 16 | * Restored compatibility with TypeScript module resolution `"node"`. 17 | 18 | 19 | ## [0.16.0](https://github.com/fasttime/Polytype/releases/tag/0.16.0) (2023-01-09) 20 | 21 | * Fixed support for base classes with parameterized types. 22 | * Improved setup instructions for TypeScript. 23 | * Added header comments to code example files. 24 | 25 | 26 | ## [0.15.0](https://github.com/fasttime/Polytype/releases/tag/0.15.0) (2022-11-17) 27 | 28 | * Using Node.js module resolution throughout the code. 29 | * Dropped support for TypeScript < 4.7. 30 | * Extended the documentation with a notice about private instance members in base classes. 31 | * Fixed the TypeScript code example. 32 | 33 | 34 | ## [0.14.1](https://github.com/fasttime/Polytype/releases/tag/0.14.1) (2022-04-17) 35 | 36 | * Update type definitions for usage with TypeScript option `exactOptionalPropertyTypes` enabled. 37 | 38 | 39 | ## [0.14.0](https://github.com/fasttime/Polytype/releases/tag/0.14.0) (2021-07-04) 40 | 41 | * Stricter type definitions: only types with an object constructor signature are now allowed as base 42 | classes. 43 | * Dropped support for TypeScript < 4.2. 44 | * Clarified parts of the code examples and the documentation. 45 | 46 | 47 | ## [0.13.2](https://github.com/fasttime/Polytype/releases/tag/0.13.2) (2021-04-18) 48 | 49 | * Late binding base constructor substitutes. 50 | * Dropped support for Firefox < 74. 51 | * Improved documentation: 52 | * Explaining value of `this` in base constructors. 53 | * Updated links. 54 | 55 | 56 | ## [0.12.0](https://github.com/fasttime/Polytype/releases/tag/0.12.0) (2021-03-02) 57 | 58 | * Adapted type definitions to allow extending abstract classes. 59 | * Improved typing of static members in classes derived from more than ten base types. 60 | * Dropped support for TypeScript < 4.0. 61 | 62 | 63 | ## [0.11.0](https://github.com/fasttime/Polytype/releases/tag/0.11.0) (2020-09-23) 64 | 65 | * Stricter own property transfer logic in class instantiation: all own properties with the same key 66 | defined by different base constructors or field initializers must be mutually redefinable now, or 67 | else a `TypeError` is thrown. 68 | This change prohibits combining configurable and unconfigurable definitions, or supplying different 69 | unconfigurable definitions unless they have the same enumerability and are all writable. 70 | Other constraints are unchanged: 71 | * If different own property definitions with the same key are found, the first definition in base 72 | class order is applied. 73 | * Own property definition order respects base class order. 74 | * Dropped support for older engines. 75 | * Updated documentation. 76 | 77 | 78 | ## [0.10.0](https://github.com/fasttime/Polytype/releases/tag/0.10.0) (2020-07-11) 79 | 80 | * Fail‐fast detection of transpilation to ES5 or earlier code. 81 | * Dropped support for Node.js < 13.7. 82 | 83 | 84 | ## [0.9.4](https://github.com/fasttime/Polytype/releases/tag/0.9.4) (2020-05-26) 85 | 86 | * Fixed inconsistent typing of `classes` in TypeScript 3.9. 87 | This change also improves the error message reported by the compiler when a call to `classes` 88 | without arguments is encountered. 89 | 90 | 91 | ## [0.9.3](https://github.com/fasttime/Polytype/releases/tag/0.9.3) (2020-05-08) 92 | 93 | * Updated documentation: 94 | * Referring to public class fields. 95 | * Using Material Design browser icons. 96 | * Some minor optimizations. 97 | 98 | 99 | ## [0.9.2](https://github.com/fasttime/Polytype/releases/tag/0.9.2) (2020-03-20) 100 | 101 | * Replaced a dead link in the documentation. 102 | * Minor stylistic update to the code examples. 103 | 104 | 105 | ## [0.9.1](https://github.com/fasttime/Polytype/releases/tag/0.9.1) (2020-03-01) 106 | 107 | * Fixed a bug with `super.class(...)` target resolution due to flawed caching. 108 | 109 | 110 | ## [0.9.0](https://github.com/fasttime/Polytype/releases/tag/0.9.0) (2020-02-29) 111 | 112 | * Fixes and breaking changes in the way `super.class(...)` works: 113 | * Inside of methods called with `super.class(...).methodName(...)` or 114 | `super.class(...)[methodKey](...)`, the value of `this` is now the same as in the calling context. 115 | * Increments, decrements and compound assignments on properties of a `super.class(...)` target 116 | work as intended. 117 | * Getting a property from a `super.class(...)` target no longer throws a `TypeError` in some 118 | special cases where the current object (`this`) has a non‐configurable own property with the same 119 | key. 120 | * Getting a property with function value from a `super.class(...)` target no longer retrieves the 121 | original function, but a proxy of it. 122 | This is a side effect of other changes. 123 | * New in documentation and code examples: *Dispatching invocations to multiple base classes*. 124 | * Minor improvements to the documentation. 125 | 126 | 127 | ## [0.8.1](https://github.com/fasttime/Polytype/releases/tag/0.8.1) (2020-02-21) 128 | 129 | * Fixed and clarified parts of the documentation. 130 | 131 | 132 | ## [0.8.0](https://github.com/fasttime/Polytype/releases/tag/0.8.0) (2020-02-08) 133 | 134 | * It is now safe to load Polytype multiple times even from mixed sources and in different realms. 135 | * Dropped support for Node.js < 13.2. 136 | 137 | 138 | ## [0.7.0](https://github.com/fasttime/Polytype/releases/tag/0.7.0) (2020-01-20) 139 | 140 | * Edge 79+ compatibility. 141 | * Dropped support for older engines and tools. 142 | * Updated package.json keywords. 143 | 144 | 145 | ## [0.6.1](https://github.com/fasttime/Polytype/releases/tag/0.6.1) (2019-10-20) 146 | 147 | * Fixed behavior of method `isPrototypOf` with cross‐realm types. 148 | 149 | 150 | ## [0.6.0](https://github.com/fasttime/Polytype/releases/tag/0.6.0) (2019-10-19) 151 | 152 | * Fixed issues with cross‐realm types. 153 | * Improved documentation. 154 | 155 | 156 | ## [0.5.0](https://github.com/fasttime/Polytype/releases/tag/0.5.0) (2019-10-11) 157 | 158 | * Clustered constructors can now be recognized as instances by the `Function` constructor. 159 | This change addresses a compatibility issue with Angular. 160 | 161 | 162 | ## [0.4.0](https://github.com/fasttime/Polytype/releases/tag/0.4.0) (2019-09-22) 163 | 164 | * Full Safari 13 compatibility – dropped support for older versions of Safari. 165 | * Dropped support for Node.js < 10.6. 166 | * Updated code examples. 167 | 168 | 169 | ## [0.3.1](https://github.com/fasttime/Polytype/releases/tag/0.3.1) (2019-07-24) 170 | 171 | * Fixed some issues with the documentation. 172 | 173 | 174 | ## [0.3.0](https://github.com/fasttime/Polytype/releases/tag/0.3.0) (2019-07-18) 175 | 176 | * Changed and documented use of `getPrototypeListOf` with class constructors. 177 | * Extended and improved documentation. 178 | 179 | 180 | ## [0.2.0](https://github.com/fasttime/Polytype/releases/tag/0.2.0) (2019-07-06) 181 | 182 | * Improved documentation. 183 | * Dropped support for older browsers and TypeScript < 3.5. 184 | 185 | 186 | ## [0.1.0](https://github.com/fasttime/Polytype/releases/tag/0.1.0) (2019-06-09) 187 | 188 | * The functions `classes` and `getPrototypeListOf` are now exported by default and no longer defined 189 | on globals. 190 | The global definitions can still be used by importing the “global” subdirectory package or by 191 | invoking the function `defineGlobally` exported by the main module. 192 | * Dropped compatibility with some obsolete versions of Chrome/Chromium and Firefox. 193 | 194 | 195 | ## [0.0.1](https://github.com/fasttime/Polytype/releases/tag/0.0.1) (2019-05-18) 196 | 197 | * Fixed merge priority of symbol own properties in constructructor targets. 198 | * Fixed an issue with function names in Edge. 199 | 200 | 201 | ## [0.0.0](https://github.com/fasttime/Polytype/releases/tag/0.0.0) (2019-05-10) 202 | 203 | First release derived from the Proxymi project. 204 | -------------------------------------------------------------------------------- /test/spec/common/classes.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert classes console createFunctionWithGetPrototypeCount maybeIt */ 3 | 4 | 'use strict'; 5 | 6 | describe 7 | ( 8 | 'classes', 9 | () => 10 | { 11 | const imitateFunctionPrototype = 12 | console.Console && 13 | console._stdout && 14 | console.timeLog && 15 | // Returns an object that fulfills all of the following requirements in Node.js 16: 16 | // * It is a non-constructor function. 17 | // * It has the same string representation as Function.prototype. 18 | // * It has no own properties. 19 | function () 20 | { 21 | const fn = new console.Console(console._stdout).timeLog; 22 | for (const key of Reflect.ownKeys(fn)) 23 | delete fn[key]; 24 | return fn; 25 | }; 26 | 27 | it 28 | ( 29 | 'has expected own properties', 30 | () => 31 | assert.hasOwnPropertyDescriptors 32 | ( 33 | classes, 34 | { 35 | length: 36 | { 37 | configurable: true, 38 | enumerable: false, 39 | value: 0, 40 | writable: false, 41 | }, 42 | name: 43 | { 44 | configurable: true, 45 | enumerable: false, 46 | value: 'classes', 47 | writable: false, 48 | }, 49 | }, 50 | ), 51 | ); 52 | 53 | it 54 | ( 55 | 'has expected prototype', 56 | () => assert.strictEqual(Object.getPrototypeOf(classes), Function.prototype), 57 | ); 58 | 59 | it 60 | ( 61 | 'cannot be called with new', 62 | // eslint-disable-next-line new-cap 63 | () => assert.throwsTypeError(() => new classes(), /\bis not a constructor\b/), 64 | ); 65 | 66 | it 67 | ( 68 | 'works with a function that is not an instance of Function', 69 | () => 70 | { 71 | const Type = Function(); 72 | Object.setPrototypeOf(Type, { }); 73 | assert.doesNotThrow(() => classes(Type)); 74 | }, 75 | ); 76 | 77 | maybeIt 78 | ( 79 | imitateFunctionPrototype, 80 | 'works with a function with a non-constructor function in the prototype chain', 81 | () => 82 | { 83 | const SuperType = imitateFunctionPrototype(); 84 | const Type = Function(); 85 | Object.setPrototypeOf(Type, SuperType); 86 | assert.doesNotThrow(() => classes(Type)); 87 | }, 88 | ); 89 | 90 | it 91 | ( 92 | 'gets property \'prototype\' only once', 93 | () => 94 | { 95 | const Foo = createFunctionWithGetPrototypeCount(); 96 | classes(Foo); 97 | assert.equal(Foo.getPrototypeCount, 1); 98 | }, 99 | ); 100 | 101 | it 102 | ( 103 | 'does not invoke its arguments', 104 | () => 105 | { 106 | function Foo() 107 | { 108 | superInvoked = true; 109 | } 110 | 111 | let superInvoked = false; 112 | classes(Foo); 113 | assert.isFalse(superInvoked); 114 | }, 115 | ); 116 | 117 | it 118 | ( 119 | 'does not invoke nonstatic field initializers', 120 | () => 121 | { 122 | let superInvoked = false; 123 | 124 | class Foo 125 | { 126 | foo = superInvoked = true; 127 | // eslint-disable-next-line no-unused-private-class-members 128 | #bar = superInvoked = true; 129 | } 130 | 131 | classes(Foo); 132 | assert.isFalse(superInvoked); 133 | }, 134 | ); 135 | 136 | describe 137 | ( 138 | 'throws a TypeError', 139 | () => 140 | { 141 | const { isPrototypeOf } = Object.prototype; 142 | 143 | it 144 | ( 145 | 'without arguments', 146 | () => assert.throwsTypeError(() => classes(), 'No superclasses specified'), 147 | ); 148 | 149 | it 150 | ( 151 | 'with a null argument', 152 | () => assert.throwsTypeError(() => classes(null), 'null is not a constructor'), 153 | ); 154 | 155 | it 156 | ( 157 | 'with a bigint argument', 158 | () => 159 | assert.throwsTypeError(() => classes(BigInt(42)), '42 is not a constructor'), 160 | ); 161 | 162 | it 163 | ( 164 | 'with a symbol argument', 165 | () => 166 | assert.throwsTypeError 167 | (() => classes(Symbol()), 'Symbol() is not a constructor'), 168 | ); 169 | 170 | it 171 | ( 172 | 'with a non-callable object argument', 173 | () => 174 | assert.throwsTypeError 175 | (() => classes({ }), '[object Object] is not a constructor'), 176 | ); 177 | 178 | it 179 | ( 180 | 'with a non-constructor callable argument', 181 | () => 182 | { 183 | const foo = () => -0; 184 | Object.defineProperties 185 | (foo, { name: { value: undefined }, prototype: { value: { } } }); 186 | assert.throwsTypeError(() => classes(foo), '() => -0 is not a constructor'); 187 | }, 188 | ); 189 | 190 | it 191 | ( 192 | 'with a bound function', 193 | () => 194 | assert.throwsTypeError 195 | ( 196 | () => classes(Array.bind()), 197 | 'Property \'prototype\' of bound Array is not an object or null', 198 | ), 199 | ); 200 | 201 | it 202 | ( 203 | 'with a function with a non-object property \'prototype\' value', 204 | () => 205 | { 206 | const foo = Function(); 207 | Object.defineProperty(foo, 'prototype', { value: 42 }); 208 | assert.throwsTypeError 209 | ( 210 | () => classes(foo), 211 | 'Property \'prototype\' of anonymous is not an object or null', 212 | ); 213 | }, 214 | ); 215 | 216 | it 217 | ( 218 | 'with a repeated argument', 219 | () => 220 | { 221 | assert.throwsTypeError 222 | (() => classes(String, Array, String), 'Duplicate superclass String'); 223 | }, 224 | ); 225 | 226 | maybeIt 227 | ( 228 | imitateFunctionPrototype, 229 | 'when property Symbol.hasInstance cannot be installed', 230 | () => 231 | { 232 | const SuperType = imitateFunctionPrototype(); 233 | Object.defineProperty(SuperType, Symbol.hasInstance, { value: Function() }); 234 | const Type = Function(); 235 | Object.setPrototypeOf(Type, SuperType); 236 | assert.throwsTypeError(() => classes(Type)); 237 | }, 238 | ); 239 | 240 | it 241 | ( 242 | 'when property \'isPrototypeOf\' cannot be installed', 243 | () => 244 | { 245 | const Type = Function(); 246 | Type.prototype = { __proto__: null, isPrototypeOf }; 247 | Object.freeze(Type.prototype); 248 | assert.throwsTypeError(() => classes(Type)); 249 | }, 250 | ); 251 | }, 252 | ); 253 | }, 254 | ); 255 | -------------------------------------------------------------------------------- /test/spec/common/clustered-constructor.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert classes createFunctionWithGetPrototypeCount */ 3 | 4 | 'use strict'; 5 | 6 | describe 7 | ( 8 | 'Clustered constructor [classes(...?)]', 9 | () => 10 | { 11 | it 12 | ( 13 | 'has unsettable prototype', 14 | () => assert.throwsTypeError(() => Object.setPrototypeOf(classes(Function()), { })), 15 | ); 16 | 17 | it 18 | ( 19 | 'has expected own properties', 20 | () => 21 | { 22 | const constructor = classes(Function()); 23 | const { get } = Object.getOwnPropertyDescriptor(constructor, 'name'); 24 | assert.hasOwnPropertyDescriptors 25 | ( 26 | constructor, 27 | { 28 | class: 29 | { 30 | configurable: false, 31 | enumerable: false, 32 | value: constructor.class, 33 | writable: false, 34 | }, 35 | length: 36 | { 37 | configurable: true, 38 | enumerable: false, 39 | value: 0, 40 | writable: false, 41 | }, 42 | name: 43 | { 44 | configurable: true, 45 | enumerable: false, 46 | get, 47 | set: undefined, 48 | }, 49 | prototype: 50 | { 51 | configurable: false, 52 | enumerable: false, 53 | value: { }, 54 | writable: false, 55 | }, 56 | }, 57 | ); 58 | }, 59 | ); 60 | 61 | it 62 | ( 63 | 'has expected name', 64 | () => 65 | { 66 | class ぁ 67 | { } 68 | 69 | class A 70 | { } 71 | 72 | class B 73 | { } 74 | 75 | class C 76 | { } 77 | 78 | Object.defineProperty(A, 'name', { value: undefined }); 79 | Object.defineProperty(B, 'name', { value: null }); 80 | Object.defineProperty(C, 'name', { value: '' }); 81 | assert.strictEqual(classes(ぁ, A, B, C).name, '(ぁ,undefined,null,)'); 82 | }, 83 | ); 84 | 85 | it 86 | ( 87 | 'cannot be called without new', 88 | () => 89 | assert.throwsTypeError 90 | (classes(Function()), 'Constructor cannot be invoked without \'new\''), 91 | ); 92 | 93 | it 94 | ( 95 | 'does not get property \'prototype\' of superclasses when called with new', 96 | () => 97 | { 98 | const A = createFunctionWithGetPrototypeCount('A'); 99 | const _A = classes(A); 100 | A.getPrototypeCount = 0; 101 | void new _A(); 102 | assert.equal(A.getPrototypeCount, 0); 103 | }, 104 | ); 105 | 106 | it 107 | ( 108 | 'invokes superconstructors in order', 109 | () => 110 | { 111 | const constructors = []; 112 | 113 | class A 114 | { 115 | constructor() 116 | { 117 | constructors.push(A); 118 | } 119 | } 120 | 121 | class B 122 | { 123 | constructor() 124 | { 125 | constructors.push(B); 126 | } 127 | } 128 | 129 | class C extends classes(A, B) 130 | { } 131 | 132 | new C(); 133 | assert.deepEqual(constructors, [A, B]); 134 | }, 135 | ); 136 | 137 | it 138 | ( 139 | 'invokes inherited nonstatic field initializers in order', 140 | () => 141 | { 142 | const values = []; 143 | 144 | class A 145 | { 146 | a = values.push('A'); 147 | } 148 | 149 | class B 150 | { 151 | b = values.push('B'); 152 | } 153 | 154 | class C extends classes(A, B) 155 | { } 156 | 157 | new C(); 158 | 159 | assert.deepEqual(values, ['A', 'B']); 160 | }, 161 | ); 162 | 163 | it 164 | ( 165 | 'defines own properties on this in order', 166 | () => 167 | { 168 | function A() 169 | { 170 | this.foo = 'foo'; 171 | Object.defineProperty(this, 'bar', { value: 'bar' }); 172 | this.baz = 'A'; 173 | } 174 | 175 | class B 176 | { 177 | baz = 'B'; 178 | [Symbol.species] = B; 179 | } 180 | 181 | function C() 182 | { 183 | this[Symbol.species] = C; 184 | } 185 | 186 | class D extends classes(A, B, C) 187 | { } 188 | 189 | const d = new D(); 190 | assert.strictEqual(d.foo, 'foo'); 191 | assert.strictEqual(d.bar, 'bar'); 192 | assert.strictEqual(d.baz, 'A'); 193 | assert.strictEqual(d[Symbol.species], B); 194 | assert.deepEqual(Reflect.ownKeys(d), ['foo', 'bar', 'baz', Symbol.species]); 195 | }, 196 | ); 197 | 198 | describe 199 | ( 200 | 'fails when a non-configurable own property is redefined', 201 | () => 202 | { 203 | it 204 | ( 205 | 'as configurable', 206 | () => 207 | { 208 | const constructors = []; 209 | 210 | class A 211 | { 212 | constructor() 213 | { 214 | Object.defineProperty(this, 'foo', { configurable: false }); 215 | constructors.push(A); 216 | } 217 | } 218 | 219 | class B 220 | { 221 | constructor() 222 | { 223 | Object.defineProperty(this, 'foo', { configurable: true }); 224 | constructors.push(B); 225 | } 226 | } 227 | 228 | class C 229 | { 230 | constructor() 231 | { 232 | constructors.push(C); 233 | } 234 | } 235 | 236 | class D extends classes(A, B, C) 237 | { } 238 | 239 | assert.throwsTypeError 240 | ( 241 | () => 242 | { 243 | new D(); 244 | }, 245 | [ 246 | 'Cannot redefine property: foo', 247 | 'can\'t redefine non-configurable property "foo"', 248 | 'Attempting to change configurable attribute of unconfigurable ' + 249 | 'property.', 250 | ], 251 | ); 252 | assert.deepEqual(constructors, [A, B, C]); 253 | }, 254 | ); 255 | 256 | it 257 | ( 258 | 'with a different access machanism', 259 | () => 260 | { 261 | const constructors = []; 262 | 263 | class A 264 | { 265 | constructor() 266 | { 267 | Object.defineProperty 268 | (this, Symbol.split, { value: A, writable: true }); 269 | constructors.push(A); 270 | } 271 | } 272 | 273 | class B 274 | { 275 | constructor() 276 | { 277 | Object.defineProperty 278 | (this, Symbol.split, { get: Function() }); 279 | constructors.push(B); 280 | } 281 | } 282 | 283 | class C 284 | { 285 | constructor() 286 | { 287 | Object.defineProperty 288 | (this, Symbol.split, { value: C, writable: true }); 289 | constructors.push(C); 290 | } 291 | } 292 | 293 | class D extends classes(A, B, C) 294 | { } 295 | 296 | assert.throwsTypeError 297 | ( 298 | () => 299 | { 300 | new D(); 301 | }, 302 | [ 303 | 'Cannot redefine property: Symbol(Symbol.split)', 304 | 'can\'t redefine non-configurable property Symbol.split', 305 | 'Attempting to change access mechanism for an unconfigurable ' + 306 | 'property.', 307 | ], 308 | ); 309 | assert.deepEqual(constructors, [A, B, C]); 310 | }, 311 | ); 312 | }, 313 | ); 314 | }, 315 | ); 316 | -------------------------------------------------------------------------------- /test/spec/common/symbol-has-instance.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* 3 | global 4 | assert 5 | classes 6 | createDeceptiveObject 7 | createFunctionFromConstructor 8 | createNullPrototypeFunction 9 | document 10 | maybeIt 11 | newRealm 12 | */ 13 | 14 | 'use strict'; 15 | 16 | describe 17 | ( 18 | '[Symbol.hasInstance]', 19 | () => 20 | { 21 | let hasInstance; 22 | 23 | before 24 | ( 25 | () => 26 | { 27 | classes(Object); 28 | hasInstance = Object[Symbol.hasInstance]; 29 | delete Function[Symbol.hasInstance]; 30 | delete Object[Symbol.hasInstance]; 31 | }, 32 | ); 33 | 34 | after 35 | ( 36 | () => 37 | { 38 | hasInstance = null; 39 | }, 40 | ); 41 | 42 | it 43 | ( 44 | 'has expected own properties', 45 | () => 46 | assert.hasOwnPropertyDescriptors 47 | ( 48 | hasInstance, 49 | { 50 | length: 51 | { 52 | configurable: true, 53 | enumerable: false, 54 | value: 1, 55 | writable: false, 56 | }, 57 | name: 58 | { 59 | configurable: true, 60 | enumerable: false, 61 | value: '[Symbol.hasInstance]', 62 | writable: false, 63 | }, 64 | }, 65 | ), 66 | ); 67 | 68 | it 69 | ( 70 | 'has expected prototype', 71 | () => assert.strictEqual(Object.getPrototypeOf(hasInstance), Function.prototype), 72 | ); 73 | 74 | it 75 | ( 76 | 'cannot be called with new', 77 | // eslint-disable-next-line new-cap 78 | () => assert.throwsTypeError(() => new hasInstance(), /\bis not a constructor\b/), 79 | ); 80 | 81 | describe 82 | ( 83 | 'is defined only on superclasses', 84 | () => 85 | { 86 | it 87 | ( 88 | 'in the same realm', 89 | () => 90 | { 91 | const A = Function(); 92 | A.prototype.constructor = null; 93 | const B = () => { }; 94 | B.prototype = { constructor: B }; 95 | const C = { __proto__: B }; 96 | C.prototype = { __proto__: B.prototype }; 97 | C.prototype.constructor = C; 98 | const D = Function(); 99 | Object.setPrototypeOf(D, C); 100 | D.prototype = { __proto__: C.prototype }; 101 | D.prototype.constructor = D; 102 | const _AD = classes(A, D); 103 | const hasInstanceDescriptor = 104 | { 105 | enumerable: false, 106 | configurable: true, 107 | value: hasInstance, 108 | writable: true, 109 | }; 110 | assert.notOwnProperty(A, Symbol.hasInstance); 111 | assert.hasOwnPropertyDescriptor 112 | (B, Symbol.hasInstance, hasInstanceDescriptor); 113 | assert.notOwnProperty(C, Symbol.hasInstance); 114 | assert.notOwnProperty(D, Symbol.hasInstance); 115 | assert.notOwnProperty(_AD, Symbol.hasInstance); 116 | }, 117 | ); 118 | 119 | maybeIt 120 | ( 121 | newRealm, 122 | 'across realms', 123 | async () => 124 | { 125 | const { Function: Functionʼ, Object: Objectʼ, classes: classesʼ } = 126 | await newRealm(true); 127 | const E = createFunctionFromConstructor(Functionʼ); 128 | const F = class extends E { }; 129 | const G = class { }; 130 | const H = class extends classesʼ(G) { }; 131 | const _FH = classes(F, H); 132 | const hasInstanceDescriptor = 133 | { 134 | enumerable: false, 135 | configurable: true, 136 | value: hasInstance, 137 | writable: true, 138 | }; 139 | assert.hasOwnPropertyDescriptor 140 | (E, Symbol.hasInstance, hasInstanceDescriptor); 141 | assert.notOwnProperty(F, Symbol.hasInstance); 142 | assert.hasOwnPropertyDescriptor 143 | (G, Symbol.hasInstance, hasInstanceDescriptor); 144 | assert.notOwnProperty(H, Symbol.hasInstance); 145 | assert.notOwnProperty(_FH, Symbol.hasInstance); 146 | assert.hasOwnPropertyDescriptor 147 | (Objectʼ, Symbol.hasInstance, hasInstanceDescriptor); 148 | assert.hasOwnPropertyDescriptor 149 | (Functionʼ, Symbol.hasInstance, hasInstanceDescriptor); 150 | }, 151 | ); 152 | }, 153 | ); 154 | 155 | it 156 | ( 157 | 'returns false when this is not callable', 158 | () => 159 | { 160 | const actual = hasInstance.call({ prototype: Object.prototype }, { }); 161 | assert.isFalse(actual); 162 | }, 163 | ); 164 | 165 | it 166 | ( 167 | 'returns false when this is null', 168 | () => 169 | { 170 | const actual = hasInstance.call(null, { }); // eslint-disable-line no-useless-call 171 | assert.isFalse(actual); 172 | }, 173 | ); 174 | 175 | it 176 | ( 177 | 'returns false when this has null prototype', 178 | () => 179 | { 180 | const actual = hasInstance.call({ __proto__: null }, { }); 181 | assert.isFalse(actual); 182 | }, 183 | ); 184 | 185 | it 186 | ( 187 | 'returns false with null argument', 188 | () => 189 | { 190 | const actual = hasInstance.call(Object, null); 191 | assert.isFalse(actual); 192 | }, 193 | ); 194 | 195 | it 196 | ( 197 | 'returns false with undefined argument', 198 | () => 199 | { 200 | const actual = hasInstance.call(Object, undefined); 201 | assert.isFalse(actual); 202 | }, 203 | ); 204 | 205 | it 206 | ( 207 | 'returns false with boolean type argument', 208 | () => 209 | { 210 | const actual = hasInstance.call(Boolean, true); 211 | assert.isFalse(actual); 212 | }, 213 | ); 214 | 215 | it 216 | ( 217 | 'returns false with number type argument', 218 | () => 219 | { 220 | const actual = hasInstance.call(Number, 1); 221 | assert.isFalse(actual); 222 | }, 223 | ); 224 | 225 | it 226 | ( 227 | 'returns false with bigint type argument', 228 | () => 229 | { 230 | const actual = hasInstance.call(Boolean, BigInt(1)); 231 | assert.isFalse(actual); 232 | }, 233 | ); 234 | 235 | it 236 | ( 237 | 'returns false with string type argument', 238 | () => 239 | { 240 | const actual = hasInstance.call(String, 'foo'); 241 | assert.isFalse(actual); 242 | }, 243 | ); 244 | 245 | it 246 | ( 247 | 'returns false with symbol type argument', 248 | () => 249 | { 250 | const actual = hasInstance.call(Symbol, Symbol.iterator); 251 | assert.isFalse(actual); 252 | }, 253 | ); 254 | 255 | maybeIt 256 | ( 257 | typeof document !== 'undefined', 258 | 'returns true with document.all', 259 | () => 260 | { 261 | const actual = hasInstance.call(Object, document.all); 262 | assert.isTrue(actual); 263 | }, 264 | ); 265 | 266 | it 267 | ( 268 | 'returns false when the argument is the prototype of this', 269 | () => 270 | { 271 | const actual = hasInstance.call(Symbol, Symbol.prototype); 272 | assert.isFalse(actual); 273 | }, 274 | ); 275 | 276 | describe 277 | ( 278 | 'when this is a function with property \'prototype\' null', 279 | () => 280 | { 281 | it 282 | ( 283 | 'returns false with a primitive argument', 284 | () => 285 | { 286 | const actual = hasInstance.call(createNullPrototypeFunction(), 1); 287 | assert.isFalse(actual); 288 | }, 289 | ); 290 | 291 | it 292 | ( 293 | 'throws a TypeError with an object argument', 294 | () => 295 | { 296 | const fn = hasInstance.bind(createNullPrototypeFunction(), { }); 297 | assert.throwsTypeError(fn); 298 | }, 299 | ); 300 | }, 301 | ); 302 | 303 | it 304 | ( 305 | 'throws a TypeError with a deceptive object', 306 | () => 307 | { 308 | const fn = Function(); 309 | { 310 | const obj = createDeceptiveObject(); 311 | assert.throwsTypeError 312 | (() => hasInstance.call(fn, obj), 'Corrupt inquiry result'); 313 | } 314 | { 315 | const obj = createDeceptiveObject(null); 316 | assert.throwsTypeError(() => hasInstance.call(fn, obj)); 317 | } 318 | }, 319 | ); 320 | }, 321 | ); 322 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | 'use strict'; 4 | 5 | const { parallel, series, src, task } = require('gulp'); 6 | const syncReadable = require('sync-readable'); 7 | 8 | async function bundle(inputPath, format, outputPath, outputPathMin) 9 | { 10 | const { homepage, version } = require('./package.json'); 11 | const { rollup } = require('rollup'); 12 | const cleanup = require('rollup-plugin-cleanup'); 13 | 14 | function addOutput(file, compact, plugins) 15 | { 16 | const outputOptions = 17 | { 18 | banner: `// Polytype ${version} – ${homepage}\n`, 19 | compact, 20 | esModule: false, 21 | file, 22 | format, 23 | plugins, 24 | }; 25 | const promise = bundle.write(outputOptions); 26 | outputPromises.push(promise); 27 | } 28 | 29 | const cleanupPlugin = cleanup({ maxEmptyLines: -1 }); 30 | const inputOptions = { input: inputPath, plugins: [cleanupPlugin] }; 31 | const bundle = await rollup(inputOptions); 32 | const outputPromises = []; 33 | addOutput(outputPath); 34 | if (outputPathMin != null) 35 | { 36 | const terser = require('@rollup/plugin-terser'); 37 | 38 | const minifyOpts = 39 | { compress: { passes: 2 }, output: { comments: (node, comment) => comment.pos === 0 } }; 40 | const terserPlugin = terser(minifyOpts); 41 | addOutput(outputPathMin, true, [terserPlugin]); 42 | } 43 | await Promise.all(outputPromises); 44 | } 45 | 46 | function readFileAsString(inputPath) 47 | { 48 | const { readFile } = require('node:fs/promises'); 49 | 50 | const promise = readFile(inputPath, 'utf8'); 51 | return promise; 52 | } 53 | 54 | task 55 | ( 56 | 'clean', 57 | async () => 58 | { 59 | const { rm } = require('node:fs/promises'); 60 | 61 | const paths = ['coverage', 'lib', 'readme.md', 'test/spec-runner.html']; 62 | const options = { force: true, recursive: true }; 63 | await Promise.all(paths.map(path => rm(path, options))); 64 | }, 65 | ); 66 | 67 | task 68 | ( 69 | 'make-ts-defs', 70 | async () => 71 | { 72 | const { mkdir, writeFile } = require('node:fs/promises'); 73 | const { version } = require('./package.json'); 74 | const Handlebars = require('handlebars'); 75 | 76 | async function writeOutput(outputPath, asModule) 77 | { 78 | const output = template({ asModule, version }); 79 | await writeFile(outputPath, output); 80 | } 81 | 82 | const mkdirPromise = mkdir('lib', { recursive: true }); 83 | const input = await readFileAsString('src/polytype.d.ts.hbs'); 84 | const template = Handlebars.compile(input, { noEscape: true }); 85 | await mkdirPromise; 86 | const promises = 87 | [ 88 | writeOutput('lib/polytype-global.d.ts', false), 89 | writeOutput('lib/polytype-module.d.ts', true), 90 | ]; 91 | await Promise.all(promises); 92 | }, 93 | ); 94 | 95 | task 96 | ( 97 | 'lint', 98 | syncReadable 99 | ( 100 | async () => 101 | { 102 | const 103 | [ 104 | { processor: tsTestProcessor }, 105 | { createConfig }, 106 | { EslintEnvProcessor }, 107 | { default: globals }, 108 | { default: gulpESLintNew }, 109 | ] = 110 | await Promise.all 111 | ( 112 | [ 113 | import('#eslint-plugin-tstest'), 114 | import('@origin-1/eslint-config'), 115 | import('eslint-plugin-eslint-env'), 116 | import('globals'), 117 | import('gulp-eslint-new'), 118 | ], 119 | ); 120 | const JS_EXAMPLE_RULES = 121 | { 122 | '@stylistic/comma-dangle': 123 | [ 124 | 'error', 125 | { 126 | 'arrays': 'always-multiline', 127 | 'objects': 'always-multiline', 128 | 'imports': 'always-multiline', 129 | 'exports': 'always-multiline', 130 | 'functions': 'only-multiline', 131 | }, 132 | ], 133 | '@stylistic/quotes': ['error', 'double'], 134 | }; 135 | const TS_EXAMPLE_RULES = { ...JS_EXAMPLE_RULES }; 136 | JS_EXAMPLE_RULES['no-unused-vars'] = 137 | TS_EXAMPLE_RULES['@typescript-eslint/no-unused-vars'] = 138 | [ 139 | 'error', 140 | { 141 | args: 'none', 142 | caughtErrors: 'all', 143 | ignoreRestSiblings: true, 144 | vars: 'local', 145 | varsIgnorePattern: '^(?:Green|WhiteUnit)Circle$', 146 | }, 147 | ]; 148 | const overrideConfig = 149 | await createConfig 150 | ( 151 | { processor: new EslintEnvProcessor() }, 152 | { 153 | files: ['**/*.js'], 154 | ignores: ['src/**/*.js'], 155 | jsVersion: 2022, 156 | languageOptions: { sourceType: 'commonjs' }, 157 | }, 158 | { 159 | files: ['src/**/*.js'], 160 | jsVersion: 2020, 161 | }, 162 | { 163 | files: ['**/*.mjs'], 164 | jsVersion: 2022, 165 | }, 166 | { 167 | files: ['**/*.ts', '**/*.tstest'], 168 | tsVersion: '4.7.0', 169 | }, 170 | { 171 | files: ['example/**/*.js'], 172 | languageOptions: { globals: { ...globals.node } }, 173 | rules: JS_EXAMPLE_RULES, 174 | }, 175 | { 176 | files: ['example/**/*.ts'], 177 | languageOptions: { globals: { ...globals.node } }, 178 | rules: TS_EXAMPLE_RULES, 179 | }, 180 | { 181 | files: ['lib/**/*.d.ts'], 182 | rules: { '@stylistic/max-len': 'off' }, 183 | }, 184 | { 185 | files: ['**/*.tstest'], 186 | languageOptions: { parserOptions: { extraFileExtensions: ['.tstest'] } }, 187 | processor: tsTestProcessor, 188 | rules: 189 | { 190 | '@typescript-eslint/no-extraneous-class': 'off', 191 | '@typescript-eslint/no-misused-new': 'off', 192 | '@typescript-eslint/no-unused-vars': 'off', 193 | '@typescript-eslint/no-useless-constructor': 'off', 194 | 'no-duplicate-imports': 'off', 195 | }, 196 | }, 197 | { 198 | files: ['**/*.json'], 199 | jsonVersion: 'standard', 200 | }, 201 | ); 202 | const stream = 203 | src 204 | ( 205 | [ 206 | '*.{js,json}', 207 | 'example/**/*.{js,ts}', 208 | 'lib/**/*.d.ts', 209 | 'src/**/*.js', 210 | 'test/**/*.{js,mjs,tstest}', 211 | ], 212 | ) 213 | .pipe 214 | ( 215 | gulpESLintNew 216 | ( 217 | { 218 | configType: 'flat', 219 | overrideConfig, 220 | overrideConfigFile: true, 221 | warnIgnored: true, 222 | }, 223 | ), 224 | ) 225 | .pipe(gulpESLintNew.format('compact')) 226 | .pipe(gulpESLintNew.failAfterError()); 227 | return stream; 228 | }, 229 | ), 230 | ); 231 | 232 | task('bundle:cjs', () => bundle('src/polytype-esm.js', 'cjs', 'lib/polytype.cjs')); 233 | 234 | task 235 | ( 236 | 'bundle:esm', 237 | () => bundle('src/polytype-esm.js', 'esm', 'lib/polytype.mjs', 'lib/polytype.min.mjs'), 238 | ); 239 | 240 | task 241 | ( 242 | 'bundle:global', 243 | () => bundle('src/polytype-global.js', 'iife', 'lib/polytype.js', 'lib/polytype.min.js'), 244 | ); 245 | 246 | task 247 | ( 248 | 'test', 249 | async () => 250 | { 251 | const [{ default: c8js }] = 252 | await Promise.all([import('c8js'), import('./test/patch-cov-source.mjs')]); 253 | await c8js 254 | ( 255 | 'test/node-spec-runner.mjs', 256 | { reporter: ['html', 'text-summary'], useC8Config: false }, 257 | ); 258 | }, 259 | ); 260 | 261 | task 262 | ( 263 | 'make-spec-runner', 264 | async () => 265 | { 266 | const { readdir, writeFile } = require('node:fs/promises'); 267 | const { extname } = require('node:path'); 268 | const Handlebars = require('handlebars'); 269 | 270 | async function getSpecs() 271 | { 272 | const filenames = await readdir('test/spec/common'); 273 | const specs = filenames.filter(filename => extname(filename) === '.js').sort(); 274 | return specs; 275 | } 276 | 277 | async function getTemplate() 278 | { 279 | const input = await readFileAsString('src/spec-runner.html.hbs'); 280 | const template = Handlebars.compile(input); 281 | return template; 282 | } 283 | 284 | const promises = [getTemplate(), getSpecs()]; 285 | const [template, specs] = await Promise.all(promises); 286 | const output = template({ specs }); 287 | await writeFile('test/spec-runner.html', output); 288 | }, 289 | ); 290 | 291 | task 292 | ( 293 | 'make-toc', 294 | async () => 295 | { 296 | const { chmod, writeFile } = require('node:fs/promises'); 297 | const { version } = require('./package.json'); 298 | const Handlebars = require('handlebars'); 299 | const toc = require('markdown-toc'); 300 | 301 | const promises = 302 | [ 303 | readFileAsString('src/readme.md.hbs'), 304 | chmod('readme.md', 0o666) 305 | .catch 306 | ( 307 | reason => 308 | { 309 | if (reason.code !== 'ENOENT') 310 | throw reason; 311 | }, 312 | ), 313 | ]; 314 | const [input] = await Promise.all(promises); 315 | const { content } = toc(input, { firsth1: false }); 316 | const template = Handlebars.compile(input, { noEscape: true }); 317 | const output = template({ toc: content, version }); 318 | await writeFile('readme.md', output, { mode: 0o444 }); 319 | }, 320 | ); 321 | 322 | task 323 | ( 324 | 'default', 325 | series 326 | ( 327 | 'clean', 328 | 'make-ts-defs', 329 | 'lint', 330 | parallel('bundle:cjs', 'bundle:esm', 'bundle:global'), 331 | 'test', 332 | parallel('make-spec-runner', 'make-toc'), 333 | ), 334 | ); 335 | 336 | task 337 | ( 338 | 'build', 339 | series 340 | ( 341 | 'make-ts-defs', 342 | parallel('bundle:cjs', 'bundle:esm', 'bundle:global'), 343 | parallel('make-spec-runner', 'make-toc'), 344 | ), 345 | ); 346 | -------------------------------------------------------------------------------- /test/spec/common/super-class.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert classes createNullPrototypeFunction */ 3 | 4 | 'use strict'; 5 | 6 | describe 7 | ( 8 | 'super.class', 9 | () => 10 | { 11 | describe 12 | ( 13 | 'in nonstatic context', 14 | () => 15 | { 16 | it 17 | ( 18 | 'has expected own properties', 19 | () => 20 | { 21 | const classValue = classes(Function()).prototype.class; 22 | assert.hasOwnPropertyDescriptors 23 | ( 24 | classValue, 25 | { 26 | length: 27 | { 28 | configurable: true, 29 | enumerable: false, 30 | value: 1, 31 | writable: false, 32 | }, 33 | name: 34 | { 35 | configurable: true, 36 | enumerable: false, 37 | value: 'class', 38 | writable: false, 39 | }, 40 | }, 41 | ); 42 | }, 43 | ); 44 | 45 | it 46 | ( 47 | 'has name "class" in second instances', 48 | () => 49 | { 50 | void classes(Object).prototype.class.name; 51 | assert.strictEqual(classes(Object).prototype.class.name, 'class'); 52 | }, 53 | ); 54 | 55 | it 56 | ( 57 | 'cannot be called with new', 58 | () => 59 | { 60 | class Foo extends classes(Object) 61 | { 62 | newSuper(type) 63 | { 64 | const superClass = super.class; 65 | new superClass(type); // eslint-disable-line new-cap 66 | } 67 | } 68 | 69 | const foo = new Foo(); 70 | assert.throwsTypeError 71 | (() => foo.newSuper(Object), /\bis not a constructor\b/); 72 | }, 73 | ); 74 | 75 | it 76 | ( 77 | 'returns a distinct proxy for any object and superclass argument', 78 | () => 79 | { 80 | class Foo 81 | { } 82 | 83 | class Bar 84 | { } 85 | 86 | class FooBar extends classes(Foo, Bar) 87 | { 88 | getSuperClass(superType) 89 | { 90 | return super.class(superType); 91 | } 92 | } 93 | 94 | const fooBar = new FooBar(); 95 | const superFoo = fooBar.getSuperClass(Foo); 96 | assert.isObject(superFoo); 97 | assert.notStrictEqual(superFoo, fooBar.getSuperClass(Bar)); 98 | assert.notStrictEqual(superFoo, new FooBar().getSuperClass(Foo)); 99 | }, 100 | ); 101 | 102 | it 103 | ( 104 | 'throws a TypeError with an invalid argument', 105 | () => 106 | { 107 | class Foo extends classes(Object) 108 | { 109 | callSuper() 110 | { 111 | super.class({ }); 112 | } 113 | } 114 | 115 | const foo = new Foo(); 116 | assert.throwsTypeError 117 | (() => foo.callSuper({ }), 'Argument is not a function'); 118 | }, 119 | ); 120 | 121 | it 122 | ( 123 | 'throws a TypeError with an indirect superclass', 124 | () => 125 | { 126 | class A 127 | { } 128 | 129 | class B extends classes(A) 130 | { } 131 | 132 | class C extends classes(B) 133 | { 134 | callSuper() 135 | { 136 | super.class(A); 137 | } 138 | } 139 | 140 | class D extends A 141 | { } 142 | 143 | class E extends classes(D) 144 | { 145 | callSuper() 146 | { 147 | super.class(A); 148 | } 149 | } 150 | 151 | const expectedMessage = 152 | 'Property \'prototype\' of argument does not match any direct superclass'; 153 | { 154 | const c = new C(); 155 | assert.throwsTypeError(() => c.callSuper(), expectedMessage); 156 | } 157 | { 158 | const e = new E(); 159 | assert.throwsTypeError(() => e.callSuper(), expectedMessage); 160 | } 161 | }, 162 | ); 163 | 164 | it 165 | ( 166 | 'throws a TypeError with a superclass with property \'prototype\' null', 167 | () => 168 | { 169 | const Foo = createNullPrototypeFunction('Foo'); 170 | 171 | class Bar extends classes(Foo) 172 | { 173 | bar() 174 | { 175 | super.class(Foo); 176 | } 177 | } 178 | 179 | const bar = new Bar(); 180 | assert.throwsTypeError 181 | ( 182 | () => bar.bar(), 183 | [ 184 | 'Property \'prototype\' of argument is not an object', 185 | 'undefined is not an object (evaluating \'super.class\')', 186 | ], 187 | ); 188 | }, 189 | ); 190 | }, 191 | ); 192 | 193 | describe 194 | ( 195 | 'in static context', 196 | () => 197 | { 198 | it 199 | ( 200 | 'has expected own properties', 201 | () => 202 | { 203 | const classValue = classes(Function()).class; 204 | assert.hasOwnPropertyDescriptors 205 | ( 206 | classValue, 207 | { 208 | length: 209 | { 210 | configurable: true, 211 | enumerable: false, 212 | value: 1, 213 | writable: false, 214 | }, 215 | name: 216 | { 217 | configurable: true, 218 | enumerable: false, 219 | value: 'class', 220 | writable: false, 221 | }, 222 | }, 223 | ); 224 | }, 225 | ); 226 | 227 | it 228 | ( 229 | 'has name "class" in second instances', 230 | () => 231 | { 232 | void classes(Object).class.name; 233 | assert.strictEqual(classes(Object).class.name, 'class'); 234 | }, 235 | ); 236 | 237 | it 238 | ( 239 | 'cannot be called with new', 240 | () => 241 | { 242 | class Foo extends classes(Object) 243 | { 244 | static newSuper(type) 245 | { 246 | const superClass = super.class; 247 | new superClass(type); // eslint-disable-line new-cap 248 | } 249 | } 250 | 251 | assert.throwsTypeError 252 | (() => Foo.newSuper(Object), /\bis not a constructor\b/); 253 | }, 254 | ); 255 | 256 | it 257 | ( 258 | 'returns a distinct proxy for any object and superclass argument', 259 | () => 260 | { 261 | class Foo 262 | { } 263 | 264 | class Bar 265 | { } 266 | 267 | class FooBar extends classes(Foo, Bar) 268 | { 269 | static getSuperClass(superType) 270 | { 271 | return super.class(superType); 272 | } 273 | } 274 | 275 | const superFoo = FooBar.getSuperClass(Foo); 276 | assert.isObject(superFoo); 277 | assert.notStrictEqual(superFoo, FooBar.getSuperClass(Bar)); 278 | assert.notStrictEqual(superFoo, { __proto__: FooBar }.getSuperClass(Foo)); 279 | }, 280 | ); 281 | 282 | it 283 | ( 284 | 'throws a TypeError with an invalid argument', 285 | () => 286 | { 287 | class Foo extends classes(Object) 288 | { 289 | static callSuper() 290 | { 291 | super.class({ }); 292 | } 293 | } 294 | 295 | assert.throwsTypeError 296 | (() => Foo.callSuper({ }), 'Argument is not a function'); 297 | }, 298 | ); 299 | 300 | it 301 | ( 302 | 'throws a TypeError with an indirect superclass', 303 | () => 304 | { 305 | class A 306 | { } 307 | 308 | class B extends classes(A) 309 | { } 310 | 311 | class C extends classes(B) 312 | { 313 | static callSuper() 314 | { 315 | super.class(A); 316 | } 317 | } 318 | 319 | class D extends A 320 | { } 321 | 322 | class E extends classes(D) 323 | { 324 | static callSuper() 325 | { 326 | super.class(A); 327 | } 328 | } 329 | 330 | const expectedMessage = 'Argument is not a direct superclass'; 331 | assert.throwsTypeError(() => C.callSuper(), expectedMessage); 332 | assert.throwsTypeError(() => E.callSuper(), expectedMessage); 333 | }, 334 | ); 335 | }, 336 | ); 337 | }, 338 | ); 339 | -------------------------------------------------------------------------------- /test/spec-helper.js: -------------------------------------------------------------------------------- 1 | /* eslint no-alert: off */ 2 | /* eslint-env mocha, shared-node-browser */ 3 | /* global Deno alert chai document location process */ 4 | 5 | 'use strict'; 6 | 7 | { 8 | function backupGlobals() 9 | { 10 | const _globalThis = globalThis; 11 | 12 | let bindDescriptor; 13 | let classesDescriptor; 14 | let fnHasInstanceDescriptor; 15 | let getPrototypeListOfDescriptor; 16 | let globalThisDescriptor; 17 | let isPrototypeOfDescriptor; 18 | let objHasInstanceDescriptor; 19 | 20 | before 21 | ( 22 | () => 23 | { 24 | classesDescriptor = Object.getOwnPropertyDescriptor(_globalThis, 'classes'); 25 | globalThisDescriptor = Object.getOwnPropertyDescriptor(_globalThis, 'globalThis'); 26 | fnHasInstanceDescriptor = 27 | Object.getOwnPropertyDescriptor(Function, Symbol.hasInstance); 28 | bindDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, 'bind'); 29 | getPrototypeListOfDescriptor = 30 | Object.getOwnPropertyDescriptor(Object, 'getPrototypeListOf'); 31 | objHasInstanceDescriptor = 32 | Object.getOwnPropertyDescriptor(Object, Symbol.hasInstance); 33 | isPrototypeOfDescriptor = 34 | Object.getOwnPropertyDescriptor(Object.prototype, 'isPrototypeOf'); 35 | }, 36 | ); 37 | 38 | afterEach 39 | ( 40 | () => 41 | { 42 | Object.defineProperty(_globalThis, 'classes', classesDescriptor); 43 | Object.defineProperty(_globalThis, 'globalThis', globalThisDescriptor); 44 | setPropertyDescriptor(Function, Symbol.hasInstance, fnHasInstanceDescriptor); 45 | // eslint-disable-next-line no-extend-native 46 | Object.defineProperty(Function.prototype, 'bind', bindDescriptor); 47 | setPropertyDescriptor(Object, 'getPrototypeListOf', getPrototypeListOfDescriptor); 48 | setPropertyDescriptor(Object, Symbol.hasInstance, objHasInstanceDescriptor); 49 | // eslint-disable-next-line no-extend-native 50 | Object.defineProperty(Object.prototype, 'isPrototypeOf', isPrototypeOfDescriptor); 51 | }, 52 | ); 53 | } 54 | 55 | function createDeceptiveObject(result = [42]) 56 | { 57 | const prototypesInquirySymbol = Symbol.for('Polytype inquiry: prototypes'); 58 | const thisSupplierInquirySymbol = Symbol.for('Polytype inquiry: this supplier'); 59 | const obj = 60 | { 61 | __proto__: 62 | { 63 | get [prototypesInquirySymbol]() 64 | { 65 | this.result = result; 66 | return undefined; 67 | }, 68 | get [thisSupplierInquirySymbol]() 69 | { 70 | this.result = result; 71 | return undefined; 72 | }, 73 | }, 74 | }; 75 | return obj; 76 | } 77 | 78 | function createFunctionFromConstructor(Function) 79 | { 80 | const fn = Function(); 81 | // Workaround for a bug in Safari. 82 | Object.setPrototypeOf(fn.prototype, Object.getPrototypeOf(Function.prototype)); 83 | return fn; 84 | } 85 | 86 | function createFunctionWithGetPrototypeCount(name) 87 | { 88 | const fn = Function(); 89 | if (name !== undefined) 90 | Object.defineProperty(fn, 'name', { value: name }); 91 | Object.defineProperty(fn, 'getPrototypeCount', { value: 0, writable: true }); 92 | const get = 93 | (target, prop, receiver) => 94 | { 95 | if (prop === 'prototype') 96 | ++target.getPrototypeCount; 97 | const value = Reflect.get(target, prop, receiver); 98 | return value; 99 | }; 100 | const proxy = new Proxy(fn, { get }); 101 | return proxy; 102 | } 103 | 104 | function createNullPrototypeFunction(name) 105 | { 106 | const fn = Function(); 107 | if (name !== undefined) 108 | Object.defineProperty(fn, 'name', { value: name }); 109 | fn.prototype = null; 110 | return fn; 111 | } 112 | 113 | function exactRegExp(...strs) 114 | { 115 | const patterns = strs.map(str => `${str.replace(/[.()[|]/g, '\\$&')}`); 116 | const pattern = patterns.length > 1 ? `(?:${patterns.join('|')})` : patterns[0]; 117 | const regExp = RegExp(`^${pattern}$`); 118 | return regExp; 119 | } 120 | 121 | function getExtension(path) 122 | { 123 | const extension = path.replace(/.*(?=\..*$)/, ''); 124 | return extension; 125 | } 126 | 127 | function getPolytypePath(extname, allowedExtnames) 128 | { 129 | if (extname == null) 130 | [extname] = allowedExtnames; 131 | else 132 | { 133 | if (!allowedExtnames.includes(extname)) 134 | throw Error(`Unsupported extension ${JSON.stringify(extname)}`); 135 | } 136 | const polytypePath = `../lib/polytype${extname}`; 137 | return polytypePath; 138 | } 139 | 140 | const maybeDescribe = (condition, ...args) => (condition ? describe : describe.skip)(...args); 141 | 142 | const maybeIt = (condition, ...args) => (condition ? it : it.skip)(...args); 143 | 144 | async function reloadPolytypeESM(polytypeURL) 145 | { 146 | const { default: reimport } = await import('./reimport.mjs'); 147 | const { defineGlobally } = await reimport(polytypeURL); 148 | defineGlobally(); 149 | return defineGlobally; 150 | } 151 | 152 | function setPropertyDescriptor(obj, key, descriptor) 153 | { 154 | if (descriptor) 155 | Object.defineProperty(obj, key, descriptor); 156 | else 157 | delete obj[key]; 158 | } 159 | 160 | const { Assertion, assert } = typeof chai !== 'undefined' ? chai : require('chai'); 161 | assert.hasOwnPropertyDescriptor = 162 | (obj, key, expectedDescriptor, msg) => 163 | { 164 | new Assertion(obj, msg, assert.hasOwnPropertyDescriptor, true) 165 | .ownPropertyDescriptor(key, expectedDescriptor); 166 | }; 167 | assert.hasOwnPropertyDescriptors = 168 | (obj, expectedDescriptors, msg) => 169 | { 170 | const actualKeys = Reflect.ownKeys(obj); 171 | const expectedKeys = Reflect.ownKeys(expectedDescriptors); 172 | assert.sameMembers(actualKeys, expectedKeys); 173 | for (const key of expectedKeys) 174 | { 175 | new Assertion(obj, msg, assert.hasOwnPropertyDescriptors, true) 176 | .ownPropertyDescriptor(key, expectedDescriptors[key]); 177 | } 178 | }; 179 | assert.throwsTypeError = 180 | (fn, expErrorMsg, msg) => 181 | { 182 | const expected = 183 | typeof expErrorMsg === 'string' ? 184 | exactRegExp(expErrorMsg) : 185 | Array.isArray(expErrorMsg) ? exactRegExp(...expErrorMsg) : expErrorMsg; 186 | new Assertion(fn, msg, assert.throwsTypeError, true).throws(TypeError, expected); 187 | }; 188 | 189 | let loadPolytype; 190 | let newRealm; 191 | let polytypeMode; 192 | if (typeof Deno !== 'undefined') 193 | { 194 | const [extname] = Deno.args; 195 | const polytypeURL = getPolytypePath(extname, ['.js', '.min.js', '.mjs', '.min.mjs']); 196 | const extension = getExtension(polytypeURL); 197 | switch (extension) 198 | { 199 | case '.js': 200 | loadPolytype = () => import(polytypeURL); 201 | break; 202 | case '.mjs': 203 | loadPolytype = () => reloadPolytypeESM(polytypeURL); 204 | break; 205 | } 206 | } 207 | else if (typeof module !== 'undefined') 208 | { 209 | const { readFile } = require('node:fs/promises'); 210 | const { SourceTextModule, createContext, runInContext } = require('node:vm'); 211 | 212 | function loadPolytypeBase() 213 | { 214 | const path = require.resolve(polytypePath); 215 | delete require.cache[path]; 216 | const returnValue = require(path); 217 | return returnValue; 218 | } 219 | 220 | const [,, extname] = process.argv; 221 | const polytypePath = 222 | getPolytypePath(extname, ['.cjs', '.js', '.min.js', '.mjs', '.min.mjs']); 223 | const extension = getExtension(polytypePath); 224 | { 225 | const path = require.resolve(polytypePath); 226 | const codePromise = readFile(path, 'utf8'); 227 | newRealm = 228 | async includePolytype => 229 | { 230 | const context = createContext(); 231 | if (includePolytype) 232 | await runInVM(await codePromise, context); 233 | const globalThat = runInContext('this', context); 234 | return globalThat; 235 | }; 236 | } 237 | let runInVM; 238 | switch (extension) 239 | { 240 | case '.js': 241 | loadPolytype = loadPolytypeBase; 242 | polytypeMode = 'global'; 243 | runInVM = runInContext; 244 | break; 245 | case '.cjs': 246 | loadPolytype = 247 | () => 248 | { 249 | const { defineGlobally } = loadPolytypeBase(); 250 | defineGlobally(); 251 | return defineGlobally; 252 | }; 253 | polytypeMode = 'module'; 254 | { 255 | const { wrap } = require('node:module'); 256 | 257 | runInVM = 258 | (code, context) => 259 | { 260 | const wrappedCode = wrap(code); 261 | const exports = { }; 262 | runInContext(wrappedCode, context)(exports); 263 | exports.defineGlobally(); 264 | }; 265 | } 266 | break; 267 | case '.mjs': 268 | loadPolytype = () => reloadPolytypeESM(polytypePath); 269 | polytypeMode = 'module'; 270 | runInVM = 271 | async (code, context) => 272 | { 273 | const sourceTextModule = new SourceTextModule(code, { context }); 274 | await sourceTextModule.link(() => null); 275 | await sourceTextModule.evaluate(); 276 | sourceTextModule.namespace.defineGlobally(); 277 | }; 278 | break; 279 | } 280 | } 281 | else 282 | { 283 | const ALLOWED_EXTENSIONS = ['.js', '.min.js', '.mjs', '.min.mjs']; 284 | 285 | const loadIFrame = 286 | iFrame => 287 | new Promise 288 | ( 289 | (resolve, reject) => 290 | { 291 | iFrame.onerror = reject; 292 | iFrame.onload = () => resolve(iFrame.contentWindow); 293 | iFrame.style.display = 'none'; 294 | document.body.appendChild(iFrame); 295 | }, 296 | ); 297 | 298 | const loadESModule = 299 | (document, src) => 300 | new Promise 301 | ( 302 | (resolve, reject) => 303 | { 304 | const script = document.createElement('script'); 305 | document.reject = 306 | ({ message }) => 307 | { 308 | reject(message); 309 | script.remove(); 310 | }; 311 | document.resolve = 312 | () => 313 | { 314 | resolve(); 315 | script.remove(); 316 | }; 317 | script.type = 'module'; 318 | script.innerText = 319 | ` 320 | (async () => 321 | { 322 | try 323 | { 324 | const { defineGlobally } = await import(${JSON.stringify(src)}); 325 | defineGlobally(); 326 | document.resolve(); 327 | } 328 | catch ({ message }) 329 | { 330 | document.reject(message); 331 | } 332 | } 333 | )(); 334 | `; 335 | document.head.appendChild(script); 336 | }, 337 | ); 338 | 339 | const loadScript = 340 | (document, src) => 341 | new Promise 342 | ( 343 | (resolve, reject) => 344 | { 345 | const script = document.createElement('script'); 346 | script.onerror = 347 | ({ message }) => 348 | { 349 | reject(message); 350 | script.remove(); 351 | }; 352 | script.onload = 353 | () => 354 | { 355 | resolve(); 356 | script.remove(); 357 | }; 358 | script.src = src; 359 | document.head.appendChild(script); 360 | }, 361 | ); 362 | 363 | newRealm = 364 | async includePolytype => 365 | { 366 | const iFrame = document.createElement('iframe'); 367 | try 368 | { 369 | const window = await loadIFrame(iFrame); 370 | if (includePolytype) 371 | await loadPolytypeInIFrame(window.document, polytypePath); 372 | return window; 373 | } 374 | finally 375 | { 376 | setTimeout(() => iFrame.remove()); 377 | } 378 | }; 379 | 380 | let polytypePath; 381 | const urlParams = new URLSearchParams(location.search); 382 | const extname = urlParams.get('extname'); 383 | try 384 | { 385 | polytypePath = getPolytypePath(extname, ALLOWED_EXTENSIONS); 386 | } 387 | catch ({ message }) 388 | { 389 | polytypePath = getPolytypePath(null, ALLOWED_EXTENSIONS); 390 | alert(message); 391 | } 392 | let loadPolytypeInIFrame; 393 | const extension = getExtension(polytypePath); 394 | switch (extension) 395 | { 396 | case '.js': 397 | loadPolytype = () => loadScript(document, polytypePath); 398 | loadPolytypeInIFrame = loadScript; 399 | break; 400 | case '.mjs': 401 | loadPolytype = () => reloadPolytypeESM(polytypePath); 402 | loadPolytypeInIFrame = loadESModule; 403 | break; 404 | } 405 | } 406 | 407 | Object.assign 408 | ( 409 | globalThis, 410 | { 411 | assert, 412 | backupGlobals, 413 | createDeceptiveObject, 414 | createFunctionFromConstructor, 415 | createFunctionWithGetPrototypeCount, 416 | createNullPrototypeFunction, 417 | loadPolytype, 418 | maybeDescribe, 419 | maybeIt, 420 | newRealm, 421 | polytypeMode, 422 | }, 423 | ); 424 | } 425 | -------------------------------------------------------------------------------- /test/spec/common/bind-call.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert classes maybeIt newRealm */ 3 | 4 | 'use strict'; 5 | 6 | describe 7 | ( 8 | '?.bind(?)', 9 | () => 10 | { 11 | function getThisAndArguments(...args) 12 | { 13 | const thisAndArguments = { this: this, arguments: args }; 14 | return thisAndArguments; 15 | } 16 | 17 | before(() => classes(Object)); 18 | 19 | describe 20 | ( 21 | 'with a regular this value', 22 | () => 23 | { 24 | it 25 | ( 26 | 'has expected own properties', 27 | () => 28 | { 29 | function πολύς(a, b, c, d, e) // eslint-disable-line no-unused-vars 30 | { } 31 | 32 | const bound_πολύς = πολύς.bind(0, 1, 2); 33 | assert.hasOwnPropertyDescriptors 34 | ( 35 | bound_πολύς, 36 | { 37 | length: 38 | { 39 | configurable: true, 40 | enumerable: false, 41 | value: 3, 42 | writable: false, 43 | }, 44 | name: 45 | { 46 | configurable: true, 47 | enumerable: false, 48 | value: 'bound πολύς', 49 | writable: false, 50 | }, 51 | }, 52 | ); 53 | }, 54 | ); 55 | 56 | it 57 | ( 58 | 'has expected prototype', 59 | () => 60 | { 61 | async function foo() 62 | { } 63 | 64 | const boundFoo = foo.bind(); 65 | assert.strictEqual 66 | ( 67 | Object.getPrototypeOf(boundFoo), 68 | Object.getPrototypeOf(foo), 69 | ); 70 | }, 71 | ); 72 | 73 | it 74 | ( 75 | 'cannot be called with new', 76 | () => 77 | { 78 | const foo = () => undefined; 79 | const boundFoo = foo.bind(); 80 | // eslint-disable-next-line new-cap 81 | assert.throwsTypeError(() => new boundFoo(), /\bis not a constructor\b/); 82 | }, 83 | ); 84 | 85 | it 86 | ( 87 | 'works with new', 88 | () => 89 | { 90 | function Foo(a, b) 91 | { 92 | this.a = a; 93 | this.b = b; 94 | } 95 | 96 | const BoundFoo = Foo.bind(41, 42); 97 | const actual = new BoundFoo(43); 98 | assert.deepStrictEqual(actual, { a: 42, b: 43 }); 99 | assert.instanceOf(actual, Foo); 100 | }, 101 | ); 102 | 103 | it 104 | ( 105 | 'works without new', 106 | () => 107 | { 108 | const thisValue = 42; 109 | const getBoundThisAndArguments = getThisAndArguments.bind(thisValue, 1); 110 | const actual = getBoundThisAndArguments(2, 3); 111 | assert.strictEqual(actual.this, thisValue); 112 | assert.deepStrictEqual(actual.arguments, [1, 2, 3]); 113 | }, 114 | ); 115 | }, 116 | ); 117 | 118 | describe 119 | ( 120 | 'with a substitute this value', 121 | () => 122 | { 123 | it 124 | ( 125 | 'has expected own properties', 126 | () => 127 | { 128 | let boundFoo; 129 | 130 | class A 131 | { 132 | constructor() 133 | { 134 | boundFoo = this.foo.bind(this, 'I', 'II', 'III', 'IV'); 135 | } 136 | 137 | // eslint-disable-next-line no-unused-vars 138 | foo(_1, _2, _3, _4, _5, _6, _7, _8) 139 | { } 140 | } 141 | 142 | class B extends classes(A) 143 | { } 144 | 145 | new B(); 146 | assert.hasOwnPropertyDescriptors 147 | ( 148 | boundFoo, 149 | { 150 | length: 151 | { 152 | configurable: true, 153 | enumerable: false, 154 | value: 4, 155 | writable: false, 156 | }, 157 | name: 158 | { 159 | configurable: true, 160 | enumerable: false, 161 | value: 'bound foo', 162 | writable: false, 163 | }, 164 | }, 165 | ); 166 | }, 167 | ); 168 | 169 | it 170 | ( 171 | 'has expected prototype', 172 | () => 173 | { 174 | let boundFoo; 175 | 176 | class A 177 | { 178 | constructor() 179 | { 180 | boundFoo = this.foo.bind(this); 181 | } 182 | 183 | async * foo() 184 | { } 185 | } 186 | 187 | class B extends classes(A) 188 | { } 189 | 190 | new B(); 191 | assert.strictEqual 192 | ( 193 | Object.getPrototypeOf(boundFoo), 194 | Object.getPrototypeOf(A.prototype.foo), 195 | ); 196 | }, 197 | ); 198 | 199 | it 200 | ( 201 | 'has consistent string representation', 202 | () => 203 | { 204 | const fns = 205 | [ 206 | function foo() // eslint-disable-line func-names 207 | { }, 208 | function () 209 | { }, 210 | async function * bar() // eslint-disable-line func-names 211 | { }, 212 | () => 42, 213 | async () => 42, // eslint-disable-line require-await 214 | class baz 215 | { }, 216 | class 217 | { }, 218 | Function(), 219 | { 220 | foo() 221 | { }, 222 | } 223 | .foo, 224 | class 225 | { 226 | bar() 227 | { } 228 | } 229 | .prototype.bar, 230 | class 231 | { 232 | static baz() 233 | { } 234 | } 235 | .baz, 236 | Object, 237 | Function.prototype, 238 | ]; 239 | 240 | const boundFnMap = new Map(); 241 | 242 | class A 243 | { 244 | constructor() 245 | { 246 | for (const fn of fns) 247 | { 248 | const boundFn = fn.bind(this); 249 | boundFnMap.set(fn, boundFn); 250 | } 251 | } 252 | } 253 | 254 | class B extends classes(A) 255 | { } 256 | 257 | new B(); 258 | for (const fn of fns) 259 | { 260 | const actual = Function.prototype.toString.call(boundFnMap.get(fn)); 261 | const expected = Function.prototype.toString.call(fn.bind(null)); 262 | const message = 263 | `expected '${actual}' to equal '${expected}' in the bound form of ` + 264 | `'${fn}'`; 265 | assert.strictEqual(actual, expected, message); 266 | } 267 | }, 268 | ); 269 | 270 | it 271 | ( 272 | 'cannot be called with new', 273 | () => 274 | { 275 | let boundFoo; 276 | 277 | class A 278 | { 279 | constructor() 280 | { 281 | boundFoo = this.foo.bind(this); 282 | } 283 | 284 | foo() 285 | { } 286 | } 287 | 288 | class B extends classes(A) 289 | { } 290 | 291 | new B(); 292 | // eslint-disable-next-line new-cap 293 | assert.throwsTypeError(() => new boundFoo(), /\bis not a constructor\b/); 294 | }, 295 | ); 296 | 297 | it 298 | ( 299 | 'works with new', 300 | () => 301 | { 302 | function Foo(a, b) 303 | { 304 | this.a = a; 305 | this.b = b; 306 | } 307 | 308 | let Bar; 309 | 310 | class A 311 | { 312 | constructor() 313 | { 314 | Bar = Foo.bind(this, 'A'); 315 | } 316 | } 317 | 318 | class B extends classes(A) 319 | { } 320 | 321 | new B(); 322 | const actual = new Bar('B'); 323 | assert.deepStrictEqual(actual, { a: 'A', b: 'B' }); 324 | assert.instanceOf(actual, Foo); 325 | }, 326 | ); 327 | 328 | describe 329 | ( 330 | 'retargets this value', 331 | () => 332 | { 333 | function test(classes) 334 | { 335 | let getBoundThisAndArguments; 336 | 337 | class A 338 | { 339 | constructor() 340 | { 341 | getBoundThisAndArguments = 342 | getThisAndArguments.bind(this, 'foo', 'bar'); 343 | } 344 | } 345 | 346 | class B extends classes(A) 347 | { } 348 | 349 | const thisValue = new B(); 350 | const actual = getBoundThisAndArguments(); 351 | assert.strictEqual(actual.this, thisValue); 352 | assert.deepStrictEqual(actual.arguments, ['foo', 'bar']); 353 | } 354 | 355 | it('in the same realm', () => test(classes)); 356 | 357 | maybeIt 358 | ( 359 | newRealm, 360 | 'in another realm', 361 | async () => 362 | { 363 | const { classes: classesʼ } = await newRealm(true); 364 | test(classesʼ); 365 | }, 366 | ); 367 | }, 368 | ); 369 | 370 | it 371 | ( 372 | 'does not retarget this value while a superconstructor is running', 373 | () => 374 | { 375 | let actual; 376 | let thisValue; 377 | let getBoundThisAndArguments; 378 | 379 | class A 380 | { 381 | constructor() 382 | { 383 | thisValue = this; 384 | getBoundThisAndArguments = getThisAndArguments.bind(this, 42); 385 | } 386 | } 387 | 388 | class B 389 | { 390 | constructor() 391 | { 392 | actual = getBoundThisAndArguments(); 393 | } 394 | } 395 | 396 | class C extends classes(A, B) 397 | { } 398 | 399 | new C(); 400 | assert.strictEqual(actual.this, thisValue); 401 | assert.deepStrictEqual(actual.arguments, [42]); 402 | }, 403 | ); 404 | 405 | it 406 | ( 407 | 'does not retarget this value if a superconstructor throws', 408 | () => 409 | { 410 | let thisValue; 411 | let getBoundThisAndArguments; 412 | 413 | class A 414 | { 415 | constructor() 416 | { 417 | thisValue = this; 418 | getBoundThisAndArguments = getThisAndArguments.bind(this); 419 | } 420 | } 421 | 422 | class B 423 | { 424 | constructor() 425 | { 426 | throw Error(); 427 | } 428 | } 429 | 430 | class C extends classes(A, B) 431 | { } 432 | 433 | try 434 | { 435 | new C(); 436 | } 437 | catch 438 | { } 439 | const actual = getBoundThisAndArguments(); 440 | assert.strictEqual(actual.this, thisValue); 441 | assert.deepStrictEqual(actual.arguments, []); 442 | }, 443 | ); 444 | }, 445 | ); 446 | }, 447 | ); 448 | -------------------------------------------------------------------------------- /test/spec/common/polytype.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* 3 | global 4 | assert 5 | backupGlobals 6 | classes 7 | createNullPrototypeFunction 8 | document 9 | loadPolytype 10 | maybeDescribe 11 | newRealm 12 | */ 13 | 14 | 'use strict'; 15 | 16 | describe 17 | ( 18 | 'Polytype', 19 | () => 20 | { 21 | function setupTestData(classes) 22 | { 23 | const callData = { }; 24 | 25 | class A 26 | { 27 | constructor() 28 | { 29 | callData.A = 30 | { 31 | args: [...arguments], // eslint-disable-line prefer-rest-params 32 | newTarget: new.target, 33 | this: this, 34 | }; 35 | } 36 | aMethod() 37 | { } 38 | set aSetOnly(arg) // eslint-disable-line accessor-pairs 39 | { } 40 | static aStatic() 41 | { } 42 | static set aStaticSetOnly(arg) // eslint-disable-line accessor-pairs 43 | { } 44 | } 45 | 46 | class B 47 | { 48 | constructor() 49 | { 50 | callData.B = 51 | { 52 | args: [...arguments], // eslint-disable-line prefer-rest-params 53 | newTarget: new.target, 54 | this: this, 55 | }; 56 | } 57 | bMethod() 58 | { } 59 | static bStatic() 60 | { } 61 | } 62 | 63 | class C extends classes(A, B) 64 | { } 65 | 66 | class D 67 | { } 68 | 69 | class E extends classes(C, D) 70 | { } 71 | 72 | const result = { A, B, C, E, callData }; 73 | return result; 74 | } 75 | 76 | describe 77 | ( 78 | 'is loaded only once', 79 | () => 80 | { 81 | backupGlobals(); 82 | 83 | it 84 | ( 85 | 'in globalThis', 86 | async () => 87 | { 88 | const expectedClasses = Function(); 89 | const globalThat = { __proto__: globalThis, classes: expectedClasses }; 90 | Object.defineProperty 91 | (globalThis, 'globalThis', { value: globalThat, configurable: true }); 92 | await loadPolytype(); 93 | assert.strictEqual(globalThat.classes, expectedClasses); 94 | delete globalThat.classes; 95 | }, 96 | ); 97 | }, 98 | ); 99 | 100 | describe 101 | ( 102 | 'provides instance level inheritance for', 103 | () => 104 | { 105 | it 106 | ( 107 | 'the in operator', 108 | () => 109 | { 110 | const { C } = setupTestData(classes); 111 | const c = new C(); 112 | assert.property(c, 'aMethod'); 113 | assert.property(c, 'bMethod'); 114 | }, 115 | ); 116 | 117 | it 118 | ( 119 | 'methods', 120 | () => 121 | { 122 | const { A, B, C } = setupTestData(classes); 123 | const c = new C(); 124 | assert.strictEqual(c.aMethod, A.prototype.aMethod); 125 | assert.strictEqual(c.bMethod, B.prototype.bMethod); 126 | }, 127 | ); 128 | 129 | it 130 | ( 131 | 'ungettable properties', 132 | () => 133 | { 134 | const { C } = setupTestData(classes); 135 | const c = new C(); 136 | assert.strictEqual(c.aSetOnly, undefined); 137 | }, 138 | ); 139 | 140 | it 141 | ( 142 | 'value overwriting', 143 | () => 144 | { 145 | const { A, C } = setupTestData(classes); 146 | A.prototype.a = 13; 147 | const c = new C(); 148 | c.a = 42; 149 | assert.hasOwnPropertyDescriptor 150 | ( 151 | c, 152 | 'a', 153 | { configurable: true, enumerable: true, value: 42, writable: true }, 154 | ); 155 | }, 156 | ); 157 | 158 | it 159 | ( 160 | 'unsettable properties', 161 | () => 162 | { 163 | class A 164 | { } 165 | 166 | class B extends classes(A) 167 | { } 168 | 169 | Object.defineProperty(A.prototype, 'a', { value: 13 }); 170 | const b = new B(); 171 | assert.throwsTypeError 172 | ( 173 | () => 174 | { 175 | b.a = 42; 176 | }, 177 | ); 178 | }, 179 | ); 180 | }, 181 | ); 182 | 183 | it 184 | ( 185 | 'allows adding new properties to an instance', 186 | () => 187 | { 188 | const { C } = setupTestData(classes); 189 | const c = new C(); 190 | c.cNewProp = 42; 191 | assert.hasOwnPropertyDescriptor 192 | ( 193 | c, 194 | 'cNewProp', 195 | { configurable: true, enumerable: true, value: 42, writable: true }, 196 | ); 197 | }, 198 | ); 199 | 200 | it 201 | ( 202 | 'allows getting undefined properties from an instance', 203 | () => 204 | { 205 | class A extends classes(Function()) 206 | { } 207 | 208 | const a = new A(); 209 | assert.isUndefined(a.unknown); 210 | }, 211 | ); 212 | 213 | describe 214 | ( 215 | 'provides class level inheritance for', 216 | () => 217 | { 218 | it 219 | ( 220 | 'the in operator', 221 | () => 222 | { 223 | const { C } = setupTestData(classes); 224 | assert.property(C, 'aStatic'); 225 | assert.property(C, 'bStatic'); 226 | }, 227 | ); 228 | 229 | it 230 | ( 231 | 'methods', 232 | () => 233 | { 234 | const { A, B, C } = setupTestData(classes); 235 | assert.strictEqual(C.aStatic, A.aStatic); 236 | assert.strictEqual(C.bStatic, B.bStatic); 237 | }, 238 | ); 239 | 240 | it 241 | ( 242 | 'ungettable properties', 243 | () => 244 | { 245 | const { C } = setupTestData(classes); 246 | assert.strictEqual(C.aStaticSetOnly, undefined); 247 | }, 248 | ); 249 | 250 | it 251 | ( 252 | 'value overwriting', 253 | () => 254 | { 255 | const { A, C } = setupTestData(classes); 256 | A.aProp = 'A'; 257 | C.aProp = 'C'; 258 | assert.hasOwnPropertyDescriptor 259 | ( 260 | C, 261 | 'aProp', 262 | { configurable: true, enumerable: true, value: 'C', writable: true }, 263 | ); 264 | }, 265 | ); 266 | 267 | it 268 | ( 269 | 'unsettable properties', 270 | () => 271 | { 272 | class A 273 | { } 274 | 275 | class B extends classes(A) 276 | { } 277 | 278 | Object.defineProperty(A, 'aProp', { value: 'A' }); 279 | assert.throwsTypeError 280 | ( 281 | () => 282 | { 283 | B.aProp = 'B'; 284 | }, 285 | ); 286 | }, 287 | ); 288 | }, 289 | ); 290 | 291 | it 292 | ( 293 | 'allows adding new properties to a class', 294 | () => 295 | { 296 | const { C } = setupTestData(classes); 297 | C.cNewProp = 42; 298 | assert.hasOwnPropertyDescriptor 299 | ( 300 | C, 301 | 'cNewProp', 302 | { configurable: true, enumerable: true, value: 42, writable: true }, 303 | ); 304 | }, 305 | ); 306 | 307 | it 308 | ( 309 | 'allows getting undefined properties from a class', 310 | () => 311 | { 312 | const { E } = setupTestData(classes); 313 | assert.isUndefined(E.unknown); 314 | }, 315 | ); 316 | 317 | maybeDescribe 318 | ( 319 | typeof document !== 'undefined', 320 | 'works well when document.all is in the prototype chain', 321 | () => 322 | { 323 | let document; 324 | let bar; 325 | let foo; 326 | 327 | beforeEach 328 | ( 329 | async () => 330 | { 331 | ({ document } = await newRealm()); 332 | const Foo = Function(); 333 | Foo.prototype = document.all; 334 | Object.getPrototypeOf(document.all).constructor = null; 335 | const Bar = 336 | class extends classes(Foo) 337 | { 338 | getFromFoo(prop) 339 | { 340 | return super.class(Foo)[prop]; 341 | } 342 | }; 343 | bar = new Bar(); 344 | foo = undefined; 345 | Object.defineProperty 346 | ( 347 | document.all, 348 | 'foo', 349 | { 350 | configurable: true, 351 | get: () => undefined, 352 | set: 353 | value => 354 | { 355 | foo = value; 356 | }, 357 | }, 358 | ); 359 | }, 360 | ); 361 | 362 | it('with getters', () => assert.strictEqual(bar[0], document.all[0])); 363 | 364 | it 365 | ( 366 | 'with setters', 367 | () => 368 | { 369 | bar.foo = 42; 370 | assert.strictEqual(foo, 42); 371 | }, 372 | ); 373 | 374 | it 375 | ( 376 | 'with super', 377 | () => 378 | { 379 | const actual = bar.getFromFoo(0); 380 | const [expected] = document.all; 381 | assert.strictEqual(actual, expected); 382 | }, 383 | ); 384 | }, 385 | ); 386 | 387 | describe 388 | ( 389 | 'super in constructor', 390 | () => 391 | { 392 | it 393 | ( 394 | 'works with array-like arguments', 395 | () => 396 | { 397 | const { C, callData } = setupTestData(classes); 398 | new C([42], ['foo', 'bar']); 399 | assert.deepEqual(callData.A.args, [42]); 400 | assert.strictEqual(Object.getPrototypeOf(callData.A.newTarget), C); 401 | assert.instanceOf(callData.A.this, C); 402 | assert.deepEqual(callData.B.args, ['foo', 'bar']); 403 | assert.strictEqual(Object.getPrototypeOf(callData.B.newTarget), C); 404 | assert.instanceOf(callData.B.this, C); 405 | }, 406 | ); 407 | 408 | it 409 | ( 410 | 'works with super-referencing arguments', 411 | () => 412 | { 413 | const { A, B, C, callData } = setupTestData(classes); 414 | new C({ super: B, arguments: [1, 2, 3] }, { super: A, arguments: ['foo'] }); 415 | assert.deepEqual(callData.A.args, ['foo']); 416 | assert.strictEqual(Object.getPrototypeOf(callData.A.newTarget), C); 417 | assert.instanceOf(callData.A.this, C); 418 | assert.deepEqual(callData.B.args, [1, 2, 3]); 419 | assert.strictEqual(Object.getPrototypeOf(callData.B.newTarget), C); 420 | assert.instanceOf(callData.B.this, C); 421 | }, 422 | ); 423 | 424 | it 425 | ( 426 | 'allows undefined in super-referencing arguments', 427 | () => 428 | { 429 | const { A, B, C, callData } = setupTestData(classes); 430 | new C({ super: A, arguments: undefined }, { super: B }); 431 | assert.deepEqual(callData.A.args, []); 432 | assert.strictEqual(Object.getPrototypeOf(callData.A.newTarget), C); 433 | assert.instanceOf(callData.A.this, C); 434 | assert.deepEqual(callData.B.args, []); 435 | assert.strictEqual(Object.getPrototypeOf(callData.B.newTarget), C); 436 | assert.instanceOf(callData.B.this, C); 437 | }, 438 | ); 439 | 440 | it 441 | ( 442 | 'does not iterate over super-referencing arguments', 443 | () => 444 | { 445 | const { A, C, callData } = setupTestData(classes); 446 | new C({ super: A, arguments: [1, 2, 3][Symbol.iterator]() }); 447 | assert.deepEqual(callData.A.args, []); 448 | }, 449 | ); 450 | 451 | describe 452 | ( 453 | 'throws a TypeError', 454 | () => 455 | { 456 | it 457 | ( 458 | 'with wrong arguments', 459 | () => 460 | { 461 | const { C } = setupTestData(classes); 462 | assert.throwsTypeError(() => new C(0), 'Invalid arguments'); 463 | }, 464 | ); 465 | 466 | it 467 | ( 468 | 'with wrong arguments in a super-referencing construct', 469 | () => 470 | { 471 | class Foo extends classes(Object) 472 | { } 473 | 474 | assert.throwsTypeError 475 | ( 476 | () => new Foo({ super: Object, arguments: false }), 477 | 'Invalid arguments for superclass Object', 478 | ); 479 | }, 480 | ); 481 | 482 | it 483 | ( 484 | 'with mixed argument styles', 485 | () => 486 | { 487 | const { A, C } = setupTestData(classes); 488 | assert.throwsTypeError 489 | (() => new C([], { super: A }), 'Mixed argument styles'); 490 | assert.throwsTypeError 491 | (() => new C(undefined, { super: A }), 'Mixed argument styles'); 492 | }, 493 | ); 494 | 495 | it 496 | ( 497 | 'with a repeated argument', 498 | () => 499 | { 500 | class Foo extends classes(Number) 501 | { } 502 | 503 | assert.throwsTypeError 504 | ( 505 | () => new Foo({ super: Number }, { super: Number }), 506 | 'Duplicate superclass Number', 507 | ); 508 | }, 509 | ); 510 | 511 | it 512 | ( 513 | 'with an invalid superclass', 514 | () => 515 | { 516 | const { A, C } = setupTestData(classes); 517 | const Foo = _ => [] <= _; 518 | Object.defineProperty(Foo, 'name', { value: '' }); 519 | assert.throwsTypeError 520 | ( 521 | () => new C({ super: A }, { super: Foo }), 522 | '_ => [] <= _ is not a direct superclass', 523 | ); 524 | }, 525 | ); 526 | }, 527 | ); 528 | }, 529 | ); 530 | 531 | describe 532 | ( 533 | 'superclass with property \'prototype\' null works with', 534 | () => 535 | { 536 | let bar; 537 | 538 | beforeEach 539 | ( 540 | () => 541 | { 542 | const Foo = createNullPrototypeFunction('Foo'); 543 | 544 | class Bar extends classes(Foo) 545 | { 546 | get bar() 547 | { 548 | return this.foo; 549 | } 550 | set bar(value) 551 | { 552 | this.foo = value; 553 | } 554 | in() 555 | { 556 | return 'foo' in this; 557 | } 558 | } 559 | 560 | bar = new Bar(); 561 | }, 562 | ); 563 | 564 | it 565 | ( 566 | 'get', 567 | () => 568 | { 569 | const foo = 42; 570 | bar.foo = foo; 571 | assert.strictEqual(bar.bar, foo); 572 | }, 573 | ); 574 | 575 | it 576 | ( 577 | 'in', 578 | () => 579 | { 580 | const foo = 43; 581 | bar.foo = foo; 582 | assert.isTrue(bar.in()); 583 | }, 584 | ); 585 | 586 | it 587 | ( 588 | 'set', 589 | () => 590 | { 591 | const foo = 44; 592 | bar.bar = foo; 593 | assert.strictEqual(bar.foo, foo); 594 | }, 595 | ); 596 | }, 597 | ); 598 | }, 599 | ); 600 | --------------------------------------------------------------------------------