Skip to content

Commit

Permalink
Fix #207 (#208)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlhaufe authored May 2, 2021
1 parent f84e8c6 commit 7721942
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 21 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## v0.20.5

* Features named with a symbol now support `@override`

## v0.20.4

* Improved error messaging with `@override` when feature is missing
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@final-hill/decorator-contracts",
"version": "0.20.4",
"version": "0.20.5",
"description": "Code Contracts for TypeScript and ECMAScript classes",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
4 changes: 2 additions & 2 deletions src/Contracted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { assert, checkedMode, Contract } from './';
import { MSG_NO_PROPERTIES, MSG_SINGLE_CONTRACT } from './Messages';

const isContracted = Symbol('isContracted'),
innerContract = Symbol('Contract');
innerContract = Symbol('innerContract');

/**
* Checks the features of the provided object for properties.
Expand Down Expand Up @@ -92,5 +92,5 @@ function Contracted<
};
}

export {isContracted};
export {isContracted, innerContract};
export default Contracted;
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import AssertionError from './AssertionError';
import {Contract, extend, invariant, Invariant, checkedMode, Rescue} from './Contract';
import Contracted from './Contracted';
import Contracted, {innerContract} from './Contracted';
import override from './override';
import assert from './assert';

export {AssertionError, Contract, Contracted, Invariant, Rescue, invariant, extend, checkedMode, override, assert};
export {AssertionError, Contract, Contracted, Invariant, Rescue, invariant, innerContract, extend, checkedMode, override, assert};
28 changes: 13 additions & 15 deletions src/lib/ClassRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { MSG_SINGLE_RETRY } from '../Messages';
import { assert, checkedMode, Contract } from '../';
import { assert, checkedMode, Contract, innerContract } from '../';
import { assertInvariants, assertDemands, CLASS_REGISTRY, Constructor, Feature, unChecked } from './';
import assertEnsures from './assertEnsures';

Expand All @@ -20,7 +20,7 @@ import assertEnsures from './assertEnsures';
* @returns {function(...args: any[]): any} - The original function augments with assertion checks
*/
function checkedFeature(
featureName: string,
featureName: PropertyKey,
fnOrig: (...args: any[]) => any,
registration: ClassRegistration
) {
Expand All @@ -36,20 +36,18 @@ function checkedFeature(

let result;
try {
let old = Object.create(null);
const old = Object.create(null);
unChecked(contract, () => {
old = registration.features.reduce((acc,{hasGetter, name}) => {
registration.features.forEach(({name, hasGetter}) => {
if(hasGetter) {
Object.defineProperty(acc, name, {value: this[name]});
Object.defineProperty(old, name, {value: this[name]});
}

return acc;
}, old);
});
});
result = fnOrig.apply(this,args);
assertEnsures(this, contract, className, featureName, old, args);
} catch(error) {
const rescue = contract.assertions[featureName]?.rescue;
const rescue = Reflect.get(contract.assertions,featureName)?.rescue;
if(rescue == null) {
assertInvariants(this, contract);
throw error;
Expand Down Expand Up @@ -77,14 +75,15 @@ function checkedFeature(

class ClassRegistration {
#features: Feature[];

contract!: Contract<any>; // Assigned by the Contracted class via this.bindContract
contractsChecked = false;

constructor(readonly Class: Constructor<any>) {
this.#features = Object.entries(Object.getOwnPropertyDescriptors(this.Class.prototype))
.filter(([key]) => key != 'constructor')
.map(([key, descriptor]) => new Feature(this, key, descriptor));
const proto = this.Class.prototype;
this.#features =
Reflect.ownKeys(proto)
.filter(key => key != 'constructor' && key !== innerContract)
.map(key => new Feature(this, key, Object.getOwnPropertyDescriptor(proto,key)!));
}

/**
Expand Down Expand Up @@ -134,8 +133,7 @@ class ClassRegistration {
}
const proto = this.Class.prototype;
this.features.forEach(feature => {
const name = String(feature.name),
{hasGetter, hasSetter, isMethod} = feature;
const {name,hasGetter, hasSetter, isMethod} = feature;

Object.defineProperty(proto, name, {
enumerable: true,
Expand Down
34 changes: 34 additions & 0 deletions src/tests/override.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,4 +369,38 @@ describe('The \'override\' decorator must have a Contracted class in it\'s ances
return new Base().toString();
}).not.toThrow();
});
});

describe('Features named with a symbol must support `@override`', () => {
test('Invalid declaration', () => {
expect(() => {
const method = Symbol('method');
class BadBase {
[method](value: number): number { return value; }
}

class Fail extends BadBase {
@override
[method](value: number): number { return value; }
}

return new Fail()[method](7);
}).toThrow(MSG_NOT_CONTRACTED);
});
test('Valid declaration', () => {
const method = Symbol('method');
@Contracted()
class Base {
[method](value: number): number { return value; }
}

class Okay extends Base {
@override
[method](value: number): number { return value; }
}

const okay = new Okay();

expect(okay[method](15)).toBe(15);
});
});

0 comments on commit 7721942

Please sign in to comment.