This repository has been archived by the owner on Sep 29, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 263
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a744f2c
commit 162bea8
Showing
4 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
import { decorate } from './private/utils'; | ||
|
||
|
||
export default function abstract(...args) { | ||
let result; | ||
|
||
if (args.length == 1) { | ||
result = abstractclass(args[0]); | ||
} | ||
else { | ||
result = decorate(handleDescriptor, args); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
|
||
function handleDescriptor(target, attr, descriptor) { | ||
let result; | ||
|
||
const desc = {...descriptor}; | ||
if (typeof descriptor.value == 'function') { | ||
result = abstractmethod(target, attr, desc); | ||
} else { | ||
throw new Error('@abstract can only be used on classes and methods'); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
|
||
function abstractmethod(target, attr, descriptor) { | ||
const result = descriptor; | ||
|
||
result.value = makeAbstract(attr); | ||
|
||
return result; | ||
} | ||
|
||
|
||
function abstractclass(target) { | ||
const result = new Function( | ||
['getSuper', 'classCallCheck', 'isConcrete'], | ||
` | ||
function ${target.name}() { | ||
classCallCheck(this, ${target.name}); | ||
var isAbstract = this.constructor.name == '${target.name}'; | ||
var isSubclassConcrete = true; | ||
if (!isAbstract) { | ||
isSubclassConcrete = isConcrete(this.constructor); | ||
} | ||
if (isAbstract || !isSubclassConcrete) { | ||
var parts = [ | ||
'abstract class ' + this.constructor.name + ' cannot be instantiated' | ||
]; | ||
if (!isSubclassConcrete) { | ||
parts.push( | ||
'did you forget to decorate using @abstract?' | ||
); | ||
} | ||
throw new TypeError(parts.join('\\n')); | ||
} | ||
getSuper(Object.getPrototypeOf(${target.name}.prototype), 'constructor', this).apply(this, arguments); | ||
}; | ||
return ${target.name}; | ||
` | ||
)(getSuper, classCallCheck, isConcrete); | ||
|
||
inherit(result, target); | ||
|
||
return result; | ||
} | ||
|
||
|
||
function isConcrete(constructor) { | ||
let result = true; | ||
|
||
// collect own properties along the inheritance chain | ||
let map = {}; | ||
let ctor = constructor; | ||
while (ctor.prototype) { | ||
let objs = [ctor, ctor.prototype]; | ||
|
||
for (let index=0; index<objs.length; index++) { | ||
for (const attr of Object.getOwnPropertyNames(objs[index])) { | ||
if (!(attr in map)) { | ||
const desc = Object.getOwnPropertyDescriptor(objs[index], attr); | ||
if (desc) { | ||
map[attr] = desc; | ||
} | ||
} | ||
} | ||
} | ||
|
||
ctor = Object.getPrototypeOf(ctor); | ||
} | ||
|
||
for (const attr in map) { | ||
const desc = map[attr]; | ||
|
||
if (desc) { | ||
const fun = desc.get || desc.set || typeof desc.value == 'function' ? desc.value : undefined; | ||
|
||
if (fun && fun[ABSTRACT]) { | ||
result = false; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
|
||
|
||
const ABSTRACT = Symbol('__core_decorators__.abstract'); | ||
|
||
|
||
function makeAbstract(attr) { | ||
const result = new Function( | ||
` | ||
return function ${attr}() { | ||
var ctor = typeof this == 'function' ? this : this.constructor; | ||
throw new Error(ctor.name + ' must implement abstract method ${attr}()'); | ||
}; | ||
` | ||
)(); | ||
|
||
Object.defineProperty(result, ABSTRACT, { | ||
configurable: false, enumerable: false, writable: false, value: true | ||
}); | ||
|
||
return result; | ||
} | ||
|
||
|
||
/* | ||
* The following helper functions were adapted from source that was | ||
* generated by the babel transpiler. | ||
*/ | ||
function getSuper(proto, property, receiver) { | ||
let result; | ||
|
||
let parent = proto || Function.prototype; | ||
while (parent) { | ||
let desc = Object.getOwnPropertyDescriptor(parent, property); | ||
if (desc === undefined) { | ||
parent = Object.getPrototypeOf(parent); | ||
} | ||
else { | ||
if ('value' in desc) { | ||
result = desc.value; | ||
} else if (desc.get) { | ||
result = desc.get.call(receiver); | ||
} | ||
break; | ||
} | ||
} | ||
|
||
return result; | ||
}; | ||
|
||
|
||
function classCallCheck(instance, constructor) { | ||
if (!(instance instanceof constructor)) { | ||
throw new TypeError('Cannot call a class as a function'); | ||
} | ||
} | ||
|
||
|
||
function inherit(subClass, superClass) { | ||
if (typeof superClass !== 'function' && superClass !== null) { | ||
throw new TypeError( | ||
'Super expression must either be null or a function, not ' + typeof superClass | ||
); | ||
} | ||
|
||
subClass.prototype = Object.create(superClass && superClass.prototype, { | ||
constructor: { | ||
value: subClass, enumerable: false, writable: true, configurable: true | ||
} | ||
}); | ||
|
||
if (superClass) { | ||
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import abstract from '../../lib/abstract'; | ||
|
||
class Base { | ||
name = 'base'; | ||
|
||
constructor(value) { | ||
this.value = value; | ||
} | ||
|
||
@abstract | ||
static whois(instance) {} | ||
|
||
@abstract | ||
speak() {} | ||
} | ||
|
||
describe('@abstract', function () { | ||
it('throws error when trying to invoke abstract static method', function () { | ||
(function () { | ||
Base.whois(); | ||
}).should.throw('Base must implement abstract method whois()'); | ||
}); | ||
it('throws error when trying to invoke abstract instance method', function () { | ||
(function () { | ||
new Base().speak(); | ||
}).should.throw('Base must implement abstract method speak()'); | ||
}); | ||
it('throws when trying to instantiate decorated class', function () { | ||
@abstract | ||
class FirstBase extends Base { | ||
} | ||
(function () { | ||
new FirstBase(); | ||
}).should.throw('abstract class FirstBase cannot be instantiated'); | ||
}); | ||
it('throws when trying to instantiate concrete class derived from abstract class not implementing abstract properties', function () { | ||
@abstract | ||
class FirstBase extends Base { | ||
} | ||
class SecondBase extends FirstBase { | ||
} | ||
(function () { | ||
new SecondBase(); | ||
}).should.throw('abstract class SecondBase cannot be instantiated\ndid you forget to decorate using @abstract?'); | ||
}); | ||
it('must support super calls on concrete class that implements all abstract properties', function () { | ||
class Concrete extends Base { | ||
constructor() { | ||
super(1); | ||
} | ||
|
||
static whois(instance) {} | ||
|
||
speak() {} | ||
} | ||
let concrete = new Concrete(); | ||
concrete.value.should.equal(1); | ||
concrete.name.should.equal('base'); | ||
}); | ||
it('must throw when user decorates instance property', function () { | ||
(function () { | ||
class Unsupported { | ||
@abstract | ||
touched; | ||
} | ||
}).should.throw('@abstract can only be used on classes and methods'); | ||
}); | ||
it('must throw when user decorates getter', function () { | ||
(function () { | ||
class Unsupported { | ||
@abstract | ||
get value() {} | ||
set value(v) {} | ||
} | ||
}).should.throw('@abstract can only be used on classes and methods'); | ||
}); | ||
it('must throw when user decorates getter', function () { | ||
(function () { | ||
class Unsupported { | ||
get value() {} | ||
@abstract | ||
set value(v) {} | ||
} | ||
}).should.throw('@abstract can only be used on classes and methods'); | ||
}); | ||
}); |