Skip to content
This repository has been archived by the owner on Sep 29, 2020. It is now read-only.

Commit

Permalink
fixes #23
Browse files Browse the repository at this point in the history
  • Loading branch information
silkentrance committed Feb 25, 2016
1 parent a744f2c commit 162bea8
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 0 deletions.
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ I *highly* recommend against using that globals build as it's quite strange you'
* [@nonenumerable](#nonenumerable)
* [@lazyInitialize](#lazyinitialize) :new:

##### For Classes and Methods
* [@abstract](#abstract) :new:

##### For Methods
* [@autobind](#autobind)
* [@deprecate](#deprecate-alias-deprecated)
Expand Down Expand Up @@ -432,5 +435,59 @@ let myConsole = {
}
```

### @abstract

Marks a given method or class for being abstract. Both static and instance methods can be declared abstract. Abstract classes, when decorated, cannot be instantiated.
When sub classing a decorated abstract class, sub classes can only be instantiated when they implement all of the abstract methods of its super classes. When calling
an abstract method, an exception will be thrown.

```js
@abstract
class Animal {
constructor(name) {
this._name = name;
}

get name() {
return this._name;
}

@abstract
static family() {}

@abstract
speak() {}
}
// Animal.family();
// throws TypeError: Animal must implement abstract method family()

@abstract
class Rodent extends Animal {
static family() {return 'rodent';}
}
// new Rodent();
// throws TypeError: abstract class Rodent cannot be instantiated

class Mouse extends Rodent {
constructor(name) {
super(name + ' Mouse');
}
speak() {return 'ieek';}
}
// let micky = new Mouse('Micky');
// micky.speak();
// returns "ieek"
// micky.name == 'Micky Mouse'
// is true

// here we forget to decorate the class
class Bird extends Animal {
static family() {return 'bird';}
}
// new Bird();
// throws TypeError: abstract class Bird cannot be instantiated
// did you forget to decorate using @abstract?
```

# Future Compatibility
Since most people can't keep up to date with specs, it's important to note that ES2016 (including the decorators spec this relies on) is in-flux and subject to breaking changes. In fact, the [biggest change is coming shortly](https://github.com/wycats/javascript-decorators/pull/36) but I am active in the appropriate communities and will be keeping this project up to date as things progress. For the most part, these changes will usually be transparent to consumers of this project--that said, core-decorators has not yet reached 1.0 and may in fact introduce breaking changes. If you'd prefer not to receive these changes, be sure to lock your dependency to [PATCH](http://semver.org/). You can track the progress of core-decorators@1.0.0 in the [The Road to 1.0](https://github.com/jayphelps/core-decorators.js/issues/15) ticket.
191 changes: 191 additions & 0 deletions src/abstract.js
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;
}
}

1 change: 1 addition & 0 deletions src/core-decorators.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export { default as decorate } from './decorate';
export { default as mixin, default as mixins } from './mixin';
export { default as lazyInitialize } from './lazy-initialize';
export { default as time } from './time';
export { default as abstract } from './abstract';
86 changes: 86 additions & 0 deletions test/unit/abstract.spec.js
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');
});
});

0 comments on commit 162bea8

Please sign in to comment.